Skip to content

Commit 460bc46

Browse files
committed
fix(core): fix CheckPoint::insert and CheckPoint::push logic
1 parent a0cb974 commit 460bc46

File tree

2 files changed

+115
-38
lines changed

2 files changed

+115
-38
lines changed

crates/core/src/checkpoint.rs

Lines changed: 100 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use core::fmt;
22
use core::ops::RangeBounds;
33

44
use alloc::sync::Arc;
5+
use alloc::vec::Vec;
56
use bitcoin::{block::Header, BlockHash};
67

78
use crate::BlockId;
@@ -284,16 +285,21 @@ where
284285
/// all entries following it. If the existing checkpoint at height is a placeholder where
285286
/// `data: None` with the same hash, then the `data` is inserted to make a complete checkpoint.
286287
/// The returned chain will have a tip of the data passed in. If the data was already present
287-
/// then this just returns `self`. This method does not create new placeholders.
288+
/// then this just returns `self`.
289+
///
290+
/// When inserting data with a `prev_blockhash` that conflicts with existing checkpoints,
291+
/// those checkpoints will be displaced and replaced with placeholders. When inserting data
292+
/// whose block hash conflicts with the `prev_blockhash` of higher checkpoints, those higher
293+
/// checkpoints will be purged.
288294
///
289295
/// # Panics
290296
///
291297
/// This panics if called with a genesis block that differs from that of `self`.
292298
#[must_use]
293299
pub fn insert(self, height: u32, data: D) -> Self {
294300
let mut cp = self.clone();
295-
let mut tail = vec![];
296-
let base = loop {
301+
let mut tail: Vec<(u32, D)> = vec![];
302+
let mut base = loop {
297303
if cp.height() == height {
298304
let same_hash = cp.hash() == data.to_blockhash();
299305
if same_hash {
@@ -322,47 +328,107 @@ where
322328
cp = cp.prev().expect("will break before genesis block");
323329
};
324330

325-
base.extend(core::iter::once((height, data)).chain(tail.into_iter().rev()))
326-
.expect("tail is in order")
327-
}
331+
if let Some(prev_hash) = data.prev_blockhash() {
332+
// Check if the new data's `prev_blockhash` conflicts with the checkpoint at height - 1.
333+
if let Some(lower_cp) = base.get(height.saturating_sub(1)) {
334+
// New data's `prev_blockhash` conflicts with existing checkpoint, so we displace
335+
// the existing checkpoint and create a placeholder.
336+
if lower_cp.hash() != prev_hash {
337+
// Find the base to link to at height - 2 or lower with actual data.
338+
// We skip placeholders because when we displace a checkpoint, we can't ensure
339+
// that placeholders below it still maintain proper chain continuity.
340+
let link_base = if height > 1 {
341+
base.find_data(height - 2)
342+
} else {
343+
None
344+
};
328345

329-
/// Puts another checkpoint onto the linked list representing the blockchain.
330-
///
331-
/// Returns an `Err(self)` if the block you are pushing on is not at a greater height that the
332-
/// one you are pushing on to.
333-
///
334-
/// If `height` is non-contiguous and `data.prev_blockhash()` is available, a placeholder is
335-
/// created at height - 1.
336-
pub fn push(self, height: u32, data: D) -> Result<Self, Self> {
337-
if self.height() < height {
338-
let mut current_cp = self.0.clone();
339-
340-
// If non-contiguous and `prev_blockhash` exists, insert a placeholder at height - 1.
341-
if height > self.height() + 1 {
342-
if let Some(prev_hash) = data.prev_blockhash() {
343-
let empty = Arc::new(CPInner {
346+
// Create a new placeholder at height - 1 with the required `prev_blockhash`.
347+
base = Self(Arc::new(CPInner {
348+
block_id: BlockId {
349+
height: height - 1,
350+
hash: prev_hash,
351+
},
352+
data: None,
353+
prev: link_base.map(|cb| cb.0),
354+
}));
355+
}
356+
} else {
357+
// No checkpoint at height - 1, but we may need to create a placeholder.
358+
if height > 0 {
359+
base = Self(Arc::new(CPInner {
344360
block_id: BlockId {
345361
height: height - 1,
346362
hash: prev_hash,
347363
},
348364
data: None,
349-
prev: Some(current_cp),
350-
});
351-
current_cp = empty;
365+
prev: base.0.prev.clone(),
366+
}));
352367
}
353368
}
369+
}
354370

355-
Ok(Self(Arc::new(CPInner {
356-
block_id: BlockId {
357-
height,
358-
hash: data.to_blockhash(),
359-
},
360-
data: Some(data),
361-
prev: Some(current_cp),
362-
})))
363-
} else {
364-
Err(self)
371+
// Check for conflicts with higher checkpoints and purge if necessary.
372+
let mut filtered_tail = Vec::new();
373+
for (tail_height, tail_data) in tail.into_iter().rev() {
374+
// Check if this tail entry's `prev_blockhash` conflicts with our new data's blockhash.
375+
if let Some(tail_prev_hash) = tail_data.prev_blockhash() {
376+
// Conflict detected, so purge this and all tail entries.
377+
if tail_prev_hash != data.to_blockhash() {
378+
break;
379+
}
380+
}
381+
filtered_tail.push((tail_height, tail_data));
382+
}
383+
384+
base.extend(core::iter::once((height, data)).chain(filtered_tail))
385+
.expect("tail is in order")
386+
}
387+
388+
/// Extends the chain by pushing a new checkpoint.
389+
///
390+
/// Returns `Err(self)` if the height is not greater than the current height, or if the data's
391+
/// `prev_blockhash` conflicts with `self`.
392+
///
393+
/// Creates a placeholder at height - 1 if the height is non-contiguous and
394+
/// `data.prev_blockhash()` is available.
395+
pub fn push(mut self, height: u32, data: D) -> Result<Self, Self> {
396+
// Reject if trying to push at or below current height - chain must grow forward
397+
if height <= self.height() {
398+
return Err(self);
399+
}
400+
401+
if let Some(prev_hash) = data.prev_blockhash() {
402+
if height == self.height() + 1 {
403+
// For contiguous height, validate that prev_blockhash matches our hash
404+
// to ensure chain continuity
405+
if self.hash() != prev_hash {
406+
return Err(self);
407+
}
408+
} else {
409+
// For non-contiguous heights, create placeholder to maintain chain linkage
410+
// This allows sparse chains while preserving block relationships
411+
self = CheckPoint(Arc::new(CPInner {
412+
block_id: BlockId {
413+
height: height
414+
.checked_sub(1)
415+
.expect("height has previous blocks so must be greater than 0"),
416+
hash: prev_hash,
417+
},
418+
data: None,
419+
prev: Some(self.0),
420+
}));
421+
}
365422
}
423+
424+
Ok(Self(Arc::new(CPInner {
425+
block_id: BlockId {
426+
height,
427+
hash: data.to_blockhash(),
428+
},
429+
data: Some(data),
430+
prev: Some(self.0),
431+
})))
366432
}
367433
}
368434

crates/core/tests/test_checkpoint.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,14 @@ fn checkpoint_insert_conflicting_prev_blockhash() {
129129

130130
// Verify chain structure
131131
assert_eq!(result.height(), 100, "tip should be at height 100");
132+
// Should have: 99 (placeholder), 100
133+
// Note: The placeholder at height 98 (from block_99's prev_blockhash) is removed
134+
// because when we displace block_99, we can't ensure the placeholder at 98 connects
135+
// properly with the new placeholder at 99.
132136
assert_eq!(
133137
result.iter().count(),
134-
3,
135-
"should have 3 checkpoints (98 placeholder, 99 placeholder, 100)"
138+
2,
139+
"should have 2 checkpoints (99 placeholder, 100)"
136140
);
137141
}
138142

@@ -303,8 +307,15 @@ fn checkpoint_insert_between_conflicting_both_sides() {
303307

304308
// Verify chain structure
305309
assert_eq!(result.height(), 5, "tip should be at height 5");
306-
// Should have: 3 (placeholder), 4 (placeholder), 5
307-
assert_eq!(result.iter().count(), 3, "should have 3 checkpoints");
310+
// Should have: 4 (placeholder), 5
311+
// Note: The placeholder at height 3 (from block_4's prev_blockhash) is removed
312+
// because when we displace block_4, we can't ensure the placeholder at 3 connects
313+
// properly with the new placeholder at 4.
314+
assert_eq!(
315+
result.iter().count(),
316+
2,
317+
"should have 2 checkpoints (4 placeholder, 5)"
318+
);
308319
}
309320

310321
/// Test that push returns Err(self) when trying to push at the same height.

0 commit comments

Comments
 (0)