]> Untitled Git - bdk/commitdiff
feat(core): Initial work on `CheckPointEntry`
author志宇 <hello@evanlinjin.me>
Fri, 9 Jan 2026 09:26:07 +0000 (17:26 +0800)
committer志宇 <hello@evanlinjin.me>
Wed, 22 Apr 2026 04:48:17 +0000 (04:48 +0000)
crates/core/src/checkpoint.rs
crates/core/src/checkpoint_entry.rs [new file with mode: 0644]
crates/core/src/lib.rs

index edb420dc06d1de40591d672804f07a3c918e6b9d..bff833621777dd5fb5c2554efe73e8005dbee1ea 100644 (file)
@@ -5,7 +5,7 @@ use alloc::sync::Arc;
 use alloc::vec::Vec;
 use bitcoin::{block::Header, BlockHash};
 
-use crate::BlockId;
+use crate::{BlockId, CheckPointEntry, CheckPointEntryIter};
 
 /// A checkpoint is a node of a reference-counted linked list of [`BlockId`]s.
 ///
@@ -210,6 +210,26 @@ impl<D> CheckPoint<D> {
     }
 }
 
+impl<D> CheckPoint<D>
+where
+    D: ToBlockHash,
+{
+    /// Iterate entries from this checkpoint in descending height.
+    pub fn entry_iter(&self) -> CheckPointEntryIter<D> {
+        self.to_entry().into_iter()
+    }
+
+    /// Transforms this checkpoint into a [`CheckPointEntry`].
+    pub fn into_entry(self) -> CheckPointEntry<D> {
+        CheckPointEntry::Occupied(self)
+    }
+
+    /// Creates a [`CheckPointEntry`].
+    pub fn to_entry(&self) -> CheckPointEntry<D> {
+        CheckPointEntry::Occupied(self.clone())
+    }
+}
+
 // Methods where `D: ToBlockHash`
 impl<D> CheckPoint<D>
 where
@@ -349,15 +369,15 @@ where
 
 /// Iterates over checkpoints backwards.
 pub struct CheckPointIter<D> {
-    current: Option<Arc<CPInner<D>>>,
+    next: Option<Arc<CPInner<D>>>,
 }
 
 impl<D> Iterator for CheckPointIter<D> {
     type Item = CheckPoint<D>;
 
     fn next(&mut self) -> Option<Self::Item> {
-        let current = self.current.clone()?;
-        self.current.clone_from(&current.prev);
+        let current = self.next.clone()?;
+        self.next.clone_from(&current.prev);
         Some(CheckPoint(current))
     }
 }
@@ -367,9 +387,7 @@ impl<D> IntoIterator for CheckPoint<D> {
     type IntoIter = CheckPointIter<D>;
 
     fn into_iter(self) -> Self::IntoIter {
-        CheckPointIter {
-            current: Some(self.0),
-        }
+        CheckPointIter { next: Some(self.0) }
     }
 }
 
diff --git a/crates/core/src/checkpoint_entry.rs b/crates/core/src/checkpoint_entry.rs
new file mode 100644 (file)
index 0000000..8e866b9
--- /dev/null
@@ -0,0 +1,207 @@
+use core::ops::RangeBounds;
+
+use bitcoin::BlockHash;
+
+use crate::{BlockId, CheckPoint, ToBlockHash};
+
+/// An entry yielded by [`CheckPointIter`].
+#[derive(Debug, Clone)]
+pub enum CheckPointEntry<D> {
+    /// A placeholder entry: there is no checkpoint stored at this height,
+    /// but the checkpoint one height above links back here via its `prev_blockhash`.
+    Placeholder {
+        /// The block ID at *this* height.
+        block_id: BlockId,
+        /// The checkpoint one height *above* that links back to `block_id`.
+        checkpoint_above: CheckPoint<D>,
+    },
+    /// A real checkpoint recorded at this height.
+    Occupied(CheckPoint<D>),
+}
+
+impl<D> CheckPointEntry<D> {
+    /// The checkpoint at this height (if any).
+    pub fn checkpoint(&self) -> Option<CheckPoint<D>> {
+        match self {
+            CheckPointEntry::Placeholder { .. } => None,
+            CheckPointEntry::Occupied(checkpoint) => Some(checkpoint.clone()),
+        }
+    }
+
+    /// Returns `true` if this entry is a placeholder (inferred from `prev_blockhash`).
+    pub fn is_placeholder(&self) -> bool {
+        matches!(self, CheckPointEntry::Placeholder { .. })
+    }
+
+    /// The checkpoint that is the *source* of this entry.
+    ///
+    /// For an `Occupied` entry, this is the checkpoint itself.
+    /// For a `Placeholder` entry, this is the checkpoint above that references this height.
+    pub fn source_checkpoint(&self) -> CheckPoint<D> {
+        match self {
+            CheckPointEntry::Placeholder {
+                checkpoint_above, ..
+            } => checkpoint_above.clone(),
+            CheckPointEntry::Occupied(checkpoint) => checkpoint.clone(),
+        }
+    }
+
+    /// Returns the checkpoint at or below this entry's height.
+    pub fn floor_checkpoint(&self) -> Option<CheckPoint<D>> {
+        match self {
+            CheckPointEntry::Placeholder {
+                checkpoint_above: linking_checkpoint,
+                ..
+            } => linking_checkpoint.prev(),
+            CheckPointEntry::Occupied(checkpoint) => Some(checkpoint.clone()),
+        }
+    }
+
+    /// Returns a reference to the data recorded at this exact height (if any).
+    pub fn data_ref(&self) -> Option<&D> {
+        match self {
+            CheckPointEntry::Placeholder { .. } => None,
+            CheckPointEntry::Occupied(checkpoint) => Some(checkpoint.data_ref()),
+        }
+    }
+
+    /// Returns the data recorded at this exact height (if any).
+    pub fn data(&self) -> Option<D>
+    where
+        D: Clone,
+    {
+        self.data_ref().cloned()
+    }
+}
+
+impl<D: ToBlockHash> CheckPointEntry<D> {
+    /// The block ID of this entry.
+    pub fn block_id(&self) -> BlockId {
+        match self {
+            CheckPointEntry::Placeholder { block_id, .. } => *block_id,
+            CheckPointEntry::Occupied(checkpoint) => checkpoint.block_id(),
+        }
+    }
+
+    /// The blockhash of this entry.
+    pub fn hash(&self) -> BlockHash {
+        self.block_id().hash
+    }
+
+    /// The block height of this entry.
+    pub fn height(&self) -> u32 {
+        self.block_id().height
+    }
+
+    /// Get the previous entry in the chain.
+    pub fn prev(&self) -> Option<Self> {
+        let checkpoint = match self {
+            Self::Placeholder {
+                checkpoint_above, ..
+            } => {
+                return checkpoint_above.prev().map(Self::Occupied);
+            }
+            Self::Occupied(checkpoint) => checkpoint,
+        };
+
+        let prev_height = checkpoint.height().checked_sub(1)?;
+
+        let prev_blockhash = match checkpoint.data_ref().prev_blockhash() {
+            Some(blockhash) => blockhash,
+            None => return checkpoint.prev().map(Self::Occupied),
+        };
+
+        if let Some(prev_checkpoint) = checkpoint.prev() {
+            if prev_checkpoint.height() == prev_height {
+                return Some(Self::Occupied(prev_checkpoint));
+            }
+        }
+
+        Some(Self::Placeholder {
+            block_id: BlockId {
+                height: prev_height,
+                hash: prev_blockhash,
+            },
+            checkpoint_above: checkpoint.clone(),
+        })
+    }
+
+    /// Iterate over checkpoint entries backwards.
+    pub fn iter(&self) -> CheckPointEntryIter<D>
+    where
+        D: Clone,
+    {
+        self.clone().into_iter()
+    }
+
+    /// Get checkpoint entry at `height`.
+    ///
+    /// Returns `None` if checkpoint at `height` does not exist.
+    pub fn get(&self, height: u32) -> Option<Self>
+    where
+        D: Clone,
+    {
+        self.range(height..=height).next()
+    }
+
+    /// Iterate checkpoints over a height range.
+    pub fn range<R>(&self, range: R) -> impl Iterator<Item = CheckPointEntry<D>>
+    where
+        D: Clone,
+        R: RangeBounds<u32>,
+    {
+        let start_bound = range.start_bound().cloned();
+        let end_bound = range.end_bound().cloned();
+        self.iter()
+            .skip_while(move |cp_entry| match end_bound {
+                core::ops::Bound::Included(inc_bound) => cp_entry.height() > inc_bound,
+                core::ops::Bound::Excluded(exc_bound) => cp_entry.height() >= exc_bound,
+                core::ops::Bound::Unbounded => false,
+            })
+            .take_while(move |cp_entry| match start_bound {
+                core::ops::Bound::Included(inc_bound) => cp_entry.height() >= inc_bound,
+                core::ops::Bound::Excluded(exc_bound) => cp_entry.height() > exc_bound,
+                core::ops::Bound::Unbounded => true,
+            })
+    }
+
+    /// Returns the entry at `height` if one exists, otherwise the nearest checkpoint at a lower
+    /// height.
+    pub fn floor_at(&self, height: u32) -> Option<Self>
+    where
+        D: Clone,
+    {
+        self.range(..=height).next()
+    }
+
+    /// Returns the entry located a number of heights below this one.
+    pub fn floor_below(&self, offset: u32) -> Option<Self>
+    where D: Clone
+    {
+        self.floor_at(self.height().checked_sub(offset)?)
+    }
+}
+
+/// Iterates over checkpoint entries backwards.
+pub struct CheckPointEntryIter<D> {
+    next: Option<CheckPointEntry<D>>,
+}
+
+impl<D: ToBlockHash> Iterator for CheckPointEntryIter<D> {
+    type Item = CheckPointEntry<D>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        let item = self.next.take()?;
+        self.next = item.prev();
+        Some(item)
+    }
+}
+
+impl<D: ToBlockHash> IntoIterator for CheckPointEntry<D> {
+    type Item = CheckPointEntry<D>;
+    type IntoIter = CheckPointEntryIter<D>;
+
+    fn into_iter(self) -> Self::IntoIter {
+        CheckPointEntryIter { next: Some(self) }
+    }
+}
index 95bebe9077fc9610e6455df786b8f2f2253cbdd5..cf02a99b0573b29b44c6823440ceb65c0fb03703 100644 (file)
@@ -65,6 +65,9 @@ pub use block_id::*;
 mod checkpoint;
 pub use checkpoint::*;
 
+mod checkpoint_entry;
+pub use checkpoint_entry::*;
+
 mod tx_update;
 pub use tx_update::*;