Skip to content

Commit e4ebed0

Browse files
committed
fix(core): fix CheckPoint::insert and CheckPoint::push logic
1 parent ed26b50 commit e4ebed0

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;
@@ -280,16 +281,21 @@ where
280281
/// all entries following it. If the existing checkpoint at height is a placeholder where
281282
/// `data: None` with the same hash, then the `data` is inserted to make a complete checkpoint.
282283
/// The returned chain will have a tip of the data passed in. If the data was already present
283-
/// then this just returns `self`. This method does not create new placeholders.
284+
/// then this just returns `self`.
285+
///
286+
/// When inserting data with a `prev_blockhash` that conflicts with existing checkpoints,
287+
/// those checkpoints will be displaced and replaced with placeholders. When inserting data
288+
/// whose block hash conflicts with the `prev_blockhash` of higher checkpoints, those higher
289+
/// checkpoints will be purged.
284290
///
285291
/// # Panics
286292
///
287293
/// This panics if called with a genesis block that differs from that of `self`.
288294
#[must_use]
289295
pub fn insert(self, height: u32, data: D) -> Self {
290296
let mut cp = self.clone();
291-
let mut tail = vec![];
292-
let base = loop {
297+
let mut tail: Vec<(u32, D)> = vec![];
298+
let mut base = loop {
293299
if cp.height() == height {
294300
let same_hash = cp.hash() == data.to_blockhash();
295301
if same_hash {
@@ -318,47 +324,107 @@ where
318324
cp = cp.prev().expect("will break before genesis block");
319325
};
320326

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

325-
/// Puts another checkpoint onto the linked list representing the blockchain.
326-
///
327-
/// Returns an `Err(self)` if the block you are pushing on is not at a greater height that the
328-
/// one you are pushing on to.
329-
///
330-
/// If `height` is non-contiguous and `data.prev_blockhash()` is available, a placeholder is
331-
/// created at height - 1.
332-
pub fn push(self, height: u32, data: D) -> Result<Self, Self> {
333-
if self.height() < height {
334-
let mut current_cp = self.0.clone();
335-
336-
// If non-contiguous and `prev_blockhash` exists, insert a placeholder at height - 1.
337-
if height > self.height() + 1 {
338-
if let Some(prev_hash) = data.prev_blockhash() {
339-
let empty = Arc::new(CPInner {
342+
// Create a new placeholder at height - 1 with the required `prev_blockhash`.
343+
base = Self(Arc::new(CPInner {
344+
block_id: BlockId {
345+
height: height - 1,
346+
hash: prev_hash,
347+
},
348+
data: None,
349+
prev: link_base.map(|cb| cb.0),
350+
}));
351+
}
352+
} else {
353+
// No checkpoint at height - 1, but we may need to create a placeholder.
354+
if height > 0 {
355+
base = Self(Arc::new(CPInner {
340356
block_id: BlockId {
341357
height: height - 1,
342358
hash: prev_hash,
343359
},
344360
data: None,
345-
prev: Some(current_cp),
346-
});
347-
current_cp = empty;
361+
prev: base.0.prev.clone(),
362+
}));
348363
}
349364
}
365+
}
350366

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

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)