Skip to content

Commit 64b8573

Browse files
authored
fix: prevent invalid header response routing (#439)
After a `Segment` reaches its checkpoint, its `current_tip_hash` equals the next segment's `start_hash`. Since segments are iterated in order, the completed segment matches first and consumes responses meant for the next segment, buffering headers past its target height. This advanced the storage tip beyond the next segment's start, causing a validation failure and the header sync getting stuck. Skip completed non-tip segments when routing incoming headers, and reject headers in `receive_headers()` if the segment is already complete.
1 parent ab4aef8 commit 64b8573

File tree

2 files changed

+47
-1
lines changed

2 files changed

+47
-1
lines changed

dash-spv/src/sync/block_headers/pipeline.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,12 @@ impl HeadersPipeline {
164164
// Find the segment that matches
165165
for (idx, segment) in self.segments.iter_mut().enumerate() {
166166
if segment.matches(&prev_hash) {
167+
// Skip completed non-tip segments. After a segment reaches its checkpoint,
168+
// its current_tip_hash equals the next segment's start_hash, so it would
169+
// incorrectly steal the next segment's responses.
170+
if segment.complete && segment.target_height.is_some() {
171+
continue;
172+
}
167173
// If tip segment was completed but receives new headers (post-sync),
168174
// reset it so take_ready_to_store() can process the new headers
169175
if segment.complete && segment.target_height.is_none() {
@@ -294,6 +300,7 @@ mod tests {
294300
use tokio::sync::mpsc::unbounded_channel;
295301

296302
use crate::network::{NetworkRequest, RequestSender};
303+
use crate::sync::block_headers::segment_state::SegmentState;
297304

298305
fn create_test_checkpoint_manager(is_testnet: bool) -> Arc<CheckpointManager> {
299306
let checkpoints = if is_testnet {
@@ -393,4 +400,43 @@ mod tests {
393400
let ready = pipeline.take_ready_to_store();
394401
assert!(ready.is_empty());
395402
}
403+
404+
#[test]
405+
fn test_completed_segment_does_not_steal_next_segment_headers() {
406+
// Create two segments which share the checkpoint hash boundary.
407+
// - Segment 0: height 0 -> target 100
408+
// - Segment 1: height 100 -> target 200
409+
let shared_hash = BlockHash::dummy(42);
410+
411+
let mut segment_0 =
412+
SegmentState::new(0, 0, BlockHash::dummy(0), Some(100), Some(shared_hash));
413+
// Mark segment 0 as complete at the checkpoint — its current_tip_hash is the shared hash
414+
segment_0.complete = true;
415+
segment_0.current_height = 100;
416+
segment_0.current_tip_hash = shared_hash;
417+
418+
let segment_1 = SegmentState::new(1, 100, shared_hash, Some(200), None);
419+
420+
// Build a pipeline with these two segments
421+
let checkpoint_manager = create_test_checkpoint_manager(true);
422+
let mut pipeline = HeadersPipeline::new(checkpoint_manager);
423+
pipeline.initialized = true;
424+
pipeline.segments = vec![segment_0, segment_1];
425+
426+
// Create a header whose prev_blockhash is the shared hash
427+
let mut header = Header::dummy(1);
428+
header.prev_blockhash = shared_hash;
429+
430+
// Mark segment 1 request as in-flight so receive works
431+
pipeline.segments[1].coordinator.mark_sent(&[shared_hash]);
432+
433+
// Route headers should go to segment 1, not the completed segment 0
434+
let matched = pipeline.receive_headers(&[header]).unwrap();
435+
assert_eq!(matched, Some(1), "Headers should route to segment 1, not completed segment 0");
436+
437+
// Segment 0 should still have no extra buffered headers
438+
assert!(pipeline.segments[0].buffered_headers.is_empty());
439+
// Segment 1 should have the header
440+
assert_eq!(pipeline.segments[1].buffered_headers.len(), 1);
441+
}
396442
}

dash-spv/src/sync/block_headers/segment_state.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ pub(super) struct SegmentState {
2020
/// Target hash (next checkpoint hash for validation).
2121
target_hash: Option<BlockHash>,
2222
/// Current tip hash for GetHeaders locator.
23-
current_tip_hash: BlockHash,
23+
pub(super) current_tip_hash: BlockHash,
2424
/// Current height reached in this segment.
2525
pub(super) current_height: u32,
2626
/// Download coordinator for tracking in-flight requests.

0 commit comments

Comments
 (0)