Skip to content

Commit b2bdf7d

Browse files
authored
chore: Move header validation into sync::headers::validation (#238)
1 parent 5d702ed commit b2bdf7d

File tree

6 files changed

+261
-271
lines changed

6 files changed

+261
-271
lines changed

dash-spv/src/client/sync_coordinator.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ use crate::error::{Result, SpvError};
1717
use crate::network::constants::MESSAGE_RECEIVE_TIMEOUT;
1818
use crate::network::NetworkManager;
1919
use crate::storage::StorageManager;
20+
use crate::sync::headers::validate_headers;
2021
use crate::types::{DetailedSyncProgress, SyncProgress};
21-
use crate::validation::validate_headers;
2222
use crate::ValidationMode;
2323
use key_wallet_manager::wallet_interface::WalletInterface;
2424
use std::time::{Duration, Instant, SystemTime};

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
//! Header synchronization with fork detection and reorganization handling.
22
33
mod manager;
4+
pub mod validation;
45

56
pub use manager::{HeaderSyncManager, ReorgConfig};
7+
pub use validation::validate_headers;

dash-spv/src/validation/headers_edge_test.rs renamed to dash-spv/src/sync/headers/validation.rs

Lines changed: 258 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,68 @@
1-
//! Edge case tests for header validation.
1+
//! Header validation functionality.
2+
3+
use dashcore::{block::Header as BlockHeader, error::Error as DashError};
4+
use std::time::Instant;
5+
6+
use crate::error::{ValidationError, ValidationResult};
7+
use crate::types::ValidationMode;
8+
9+
/// Validate a chain of headers considering the validation mode.
10+
pub fn validate_headers(headers: &[BlockHeader], mode: ValidationMode) -> ValidationResult<()> {
11+
if mode == ValidationMode::None {
12+
tracing::debug!("Skipping header validation: disabled");
13+
return Ok(());
14+
}
15+
16+
if headers.is_empty() {
17+
tracing::debug!("Skipping header validation: empty headers");
18+
return Ok(());
19+
}
20+
21+
let start = Instant::now();
22+
23+
let mut prev_header_hash = None;
24+
for header in headers {
25+
// Check chain continuity if we have previous header
26+
if let Some(prev) = prev_header_hash {
27+
if header.prev_blockhash != prev {
28+
return Err(ValidationError::InvalidHeaderChain(
29+
"Header does not connect to previous header".to_string(),
30+
));
31+
}
32+
}
33+
34+
if mode == ValidationMode::Full {
35+
// Validate proof of work with X11 hashing
36+
let target = header.target();
37+
if let Err(e) = header.validate_pow(target) {
38+
return match e {
39+
DashError::BlockBadProofOfWork => Err(ValidationError::InvalidProofOfWork),
40+
DashError::BlockBadTarget => {
41+
Err(ValidationError::InvalidHeaderChain("Invalid target".to_string()))
42+
}
43+
_ => Err(ValidationError::InvalidHeaderChain(format!(
44+
"PoW validation error: {:?}",
45+
e
46+
))),
47+
};
48+
}
49+
}
50+
51+
prev_header_hash = Some(header.block_hash());
52+
}
53+
54+
tracing::debug!(
55+
"Header chain validation passed for {} headers in mode: {:?}, duration: {:?}",
56+
headers.len(),
57+
mode,
58+
start.elapsed(),
59+
);
60+
Ok(())
61+
}
262

363
#[cfg(test)]
464
mod tests {
5-
use super::super::validate_headers;
65+
use super::validate_headers;
666
use crate::error::ValidationError;
767
use crate::types::ValidationMode;
868
use dashcore::{
@@ -12,6 +72,23 @@ mod tests {
1272
};
1373
use dashcore_hashes::Hash;
1474

75+
/// Create a test header with given parameters
76+
fn create_test_header(
77+
prev_hash: dashcore::BlockHash,
78+
nonce: u32,
79+
bits: u32,
80+
time: u32,
81+
) -> BlockHeader {
82+
BlockHeader {
83+
version: Version::from_consensus(0x20000000),
84+
prev_blockhash: prev_hash,
85+
merkle_root: dashcore::TxMerkleNode::from_byte_array([0; 32]),
86+
time,
87+
bits: dashcore::CompactTarget::from_consensus(bits),
88+
nonce,
89+
}
90+
}
91+
1592
/// Create a test header with specific parameters
1693
fn create_test_header_with_params(
1794
version: u32,
@@ -31,6 +108,185 @@ mod tests {
31108
}
32109
}
33110

111+
// ==================== Basic Tests ====================
112+
113+
#[test]
114+
fn test_validation_mode_none_always_passes() {
115+
let header = create_test_header(
116+
dashcore::BlockHash::from_raw_hash(dashcore_hashes::hash_x11::Hash::from_byte_array(
117+
[0; 32],
118+
)),
119+
0,
120+
0x1e0fffff,
121+
1234567890,
122+
);
123+
124+
// Should pass with no previous header
125+
assert!(validate_headers(&[header], ValidationMode::None).is_ok());
126+
127+
// Should pass even with invalid chain continuity
128+
let prev_header = create_test_header(
129+
dashcore::BlockHash::from_raw_hash(dashcore_hashes::hash_x11::Hash::from_byte_array(
130+
[1; 32],
131+
)),
132+
1,
133+
0x1e0fffff,
134+
1234567890,
135+
);
136+
assert!(validate_headers(&[prev_header, header], ValidationMode::None).is_ok());
137+
}
138+
139+
#[test]
140+
fn test_basic_validation_chain_continuity() {
141+
// Create two headers that connect properly
142+
let header1 = create_test_header(
143+
dashcore::BlockHash::from_raw_hash(dashcore_hashes::hash_x11::Hash::from_byte_array(
144+
[0; 32],
145+
)),
146+
1,
147+
0x1e0fffff,
148+
1234567890,
149+
);
150+
let header2 = create_test_header(header1.block_hash(), 2, 0x1e0fffff, 1234567900);
151+
152+
// Should pass when headers connect
153+
assert!(validate_headers(&[header1, header2], ValidationMode::Basic).is_ok());
154+
155+
// Should fail when headers don't connect
156+
let disconnected_header = create_test_header(
157+
dashcore::BlockHash::from_raw_hash(dashcore_hashes::hash_x11::Hash::from_byte_array(
158+
[99; 32],
159+
)),
160+
3,
161+
0x1e0fffff,
162+
1234567910,
163+
);
164+
let result = validate_headers(&[header1, disconnected_header], ValidationMode::Basic);
165+
assert!(matches!(result, Err(ValidationError::InvalidHeaderChain(_))));
166+
}
167+
168+
#[test]
169+
fn test_basic_validation_no_pow_check() {
170+
// Create header with invalid PoW (would fail full validation)
171+
let header = create_test_header(
172+
dashcore::BlockHash::from_raw_hash(dashcore_hashes::hash_x11::Hash::from_byte_array(
173+
[0; 32],
174+
)),
175+
0, // Invalid nonce that won't produce valid PoW
176+
0x1e0fffff,
177+
1234567890,
178+
);
179+
180+
// Should pass basic validation (no PoW check)
181+
assert!(validate_headers(&[header], ValidationMode::Basic).is_ok());
182+
}
183+
184+
#[test]
185+
fn test_full_validation_includes_pow() {
186+
// Create header with invalid PoW
187+
let header = create_test_header(
188+
dashcore::BlockHash::from_raw_hash(dashcore_hashes::hash_x11::Hash::from_byte_array(
189+
[0; 32],
190+
)),
191+
0, // Invalid nonce
192+
0x1d00ffff, // Difficulty that requires real PoW
193+
1234567890,
194+
);
195+
196+
// Should fail full validation due to invalid PoW
197+
let result = validate_headers(&[header], ValidationMode::Full);
198+
assert!(matches!(result, Err(ValidationError::InvalidProofOfWork)));
199+
}
200+
201+
#[test]
202+
fn test_validate_headers_empty() {
203+
for mode in [ValidationMode::None, ValidationMode::Basic, ValidationMode::Full] {
204+
let headers: Vec<BlockHeader> = vec![];
205+
// Empty chain should pass
206+
assert!(validate_headers(&headers, mode).is_ok());
207+
}
208+
}
209+
210+
#[test]
211+
fn test_validate_headers_basic_single_header() {
212+
let header = create_test_header(
213+
dashcore::BlockHash::from_raw_hash(dashcore_hashes::hash_x11::Hash::from_byte_array(
214+
[0; 32],
215+
)),
216+
1,
217+
0x1e0fffff,
218+
1234567890,
219+
);
220+
221+
// Single header should pass (no chain validation needed)
222+
assert!(validate_headers(&[header], ValidationMode::Basic).is_ok());
223+
}
224+
225+
#[test]
226+
fn test_validate_headers_basic_valid_chain() {
227+
// Create a valid chain of headers
228+
let mut headers = vec![];
229+
let mut prev_hash = dashcore::BlockHash::from_raw_hash(
230+
dashcore_hashes::hash_x11::Hash::from_byte_array([0; 32]),
231+
);
232+
233+
for i in 0..5 {
234+
let header = create_test_header(prev_hash, i, 0x1e0fffff, 1234567890 + i * 600);
235+
prev_hash = header.block_hash();
236+
headers.push(header);
237+
}
238+
239+
// Valid chain should pass
240+
assert!(validate_headers(&headers, ValidationMode::Basic).is_ok());
241+
}
242+
243+
#[test]
244+
fn test_validate_headers_basic_broken_chain() {
245+
// Create a chain with a break in the middle
246+
let header1 = create_test_header(
247+
dashcore::BlockHash::from_raw_hash(dashcore_hashes::hash_x11::Hash::from_byte_array(
248+
[0; 32],
249+
)),
250+
1,
251+
0x1e0fffff,
252+
1234567890,
253+
);
254+
let header2 = create_test_header(header1.block_hash(), 2, 0x1e0fffff, 1234567900);
255+
let header3 = create_test_header(
256+
dashcore::BlockHash::from_raw_hash(dashcore_hashes::hash_x11::Hash::from_byte_array(
257+
[99; 32],
258+
)), // Broken link
259+
3,
260+
0x1e0fffff,
261+
1234567910,
262+
);
263+
264+
let headers = vec![header1, header2, header3];
265+
266+
// Should fail due to broken chain
267+
let result = validate_headers(&headers, ValidationMode::Basic);
268+
assert!(matches!(result, Err(ValidationError::InvalidHeaderChain(_))));
269+
}
270+
271+
#[test]
272+
fn test_validate_headers_full_with_pow() {
273+
// Create headers with invalid PoW
274+
let header1 = create_test_header(
275+
dashcore::BlockHash::from_raw_hash(dashcore_hashes::hash_x11::Hash::from_byte_array(
276+
[0; 32],
277+
)),
278+
0, // Invalid nonce
279+
0x1d00ffff, // Difficulty that requires real PoW
280+
1234567890,
281+
);
282+
283+
// Should fail when PoW validation is enabled
284+
let result = validate_headers(&[header1], ValidationMode::Full);
285+
assert!(matches!(result, Err(ValidationError::InvalidProofOfWork)));
286+
}
287+
288+
// ==================== Edge Case Tests ====================
289+
34290
#[test]
35291
fn test_genesis_block_validation() {
36292
for network in [Network::Dash, Network::Testnet, Network::Regtest] {

dash-spv/src/validation/headers.rs

Lines changed: 0 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +0,0 @@
1-
//! Header validation functionality.
2-
3-
use dashcore::{block::Header as BlockHeader, error::Error as DashError};
4-
use std::time::Instant;
5-
6-
use crate::error::{ValidationError, ValidationResult};
7-
use crate::types::ValidationMode;
8-
9-
/// Validate a chain of headers considering the validation mode.
10-
pub fn validate_headers(headers: &[BlockHeader], mode: ValidationMode) -> ValidationResult<()> {
11-
if mode == ValidationMode::None {
12-
tracing::debug!("Skipping header validation: disabled");
13-
return Ok(());
14-
}
15-
16-
let start = Instant::now();
17-
18-
let mut prev_block_hash = None;
19-
for header in headers {
20-
// Check chain continuity if we have previous header
21-
if let Some(prev) = prev_block_hash {
22-
if header.prev_blockhash != prev {
23-
return Err(ValidationError::InvalidHeaderChain(
24-
"Header does not connect to previous header".to_string(),
25-
));
26-
}
27-
}
28-
29-
if mode == ValidationMode::Full {
30-
// Validate proof of work with X11 hashing
31-
let target = header.target();
32-
if let Err(e) = header.validate_pow(target) {
33-
return match e {
34-
DashError::BlockBadProofOfWork => Err(ValidationError::InvalidProofOfWork),
35-
DashError::BlockBadTarget => {
36-
Err(ValidationError::InvalidHeaderChain("Invalid target".to_string()))
37-
}
38-
_ => Err(ValidationError::InvalidHeaderChain(format!(
39-
"PoW validation error: {:?}",
40-
e
41-
))),
42-
};
43-
}
44-
}
45-
46-
prev_block_hash = Some(header.block_hash());
47-
}
48-
49-
tracing::debug!(
50-
"Header chain validation passed for {} headers in mode: {:?}, duration: {:?}",
51-
headers.len(),
52-
mode,
53-
start.elapsed(),
54-
);
55-
Ok(())
56-
}
57-
58-
#[cfg(test)]
59-
#[path = "headers_test.rs"]
60-
mod headers_test;
61-
62-
#[cfg(test)]
63-
#[path = "headers_edge_test.rs"]
64-
mod headers_edge_test;

0 commit comments

Comments
 (0)