-
Notifications
You must be signed in to change notification settings - Fork 418
Add median-time-past (MTP) calculation to CheckPoint #2037
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ use core::fmt; | |
| use core::ops::RangeBounds; | ||
|
|
||
| use alloc::sync::Arc; | ||
| use alloc::vec::Vec; | ||
| use bitcoin::{block::Header, BlockHash}; | ||
|
|
||
| use crate::BlockId; | ||
|
|
@@ -60,14 +61,22 @@ impl<D> Drop for CPInner<D> { | |
|
|
||
| /// Trait that converts [`CheckPoint`] `data` to [`BlockHash`]. | ||
| /// | ||
| /// Implementations of [`ToBlockHash`] must always return the block’s consensus-defined hash. If | ||
| /// Implementations of [`ToBlockHash`] must always return the block's consensus-defined hash. If | ||
| /// your type contains extra fields (timestamps, metadata, etc.), these must be ignored. For | ||
| /// example, [`BlockHash`] trivially returns itself, [`Header`] calls its `block_hash()`, and a | ||
| /// wrapper type around a [`Header`] should delegate to the header’s hash rather than derive one | ||
| /// wrapper type around a [`Header`] should delegate to the header's hash rather than derive one | ||
| /// from other fields. | ||
| pub trait ToBlockHash { | ||
| /// Returns the [`BlockHash`] for the associated [`CheckPoint`] `data` type. | ||
| fn to_blockhash(&self) -> BlockHash; | ||
|
|
||
| /// Returns the block time if available for the data type. | ||
| /// | ||
| /// Default implementation returns `None`. Data types that include timestamp information | ||
| /// (such as [`Header`]) should override this method. | ||
| fn block_time(&self) -> Option<u32> { | ||
| None | ||
| } | ||
| } | ||
|
|
||
| impl ToBlockHash for BlockHash { | ||
|
|
@@ -80,6 +89,10 @@ impl ToBlockHash for Header { | |
| fn to_blockhash(&self) -> BlockHash { | ||
| self.block_hash() | ||
| } | ||
|
|
||
| fn block_time(&self) -> Option<u32> { | ||
| Some(self.time) | ||
| } | ||
| } | ||
|
|
||
| impl<D> PartialEq for CheckPoint<D> { | ||
|
|
@@ -195,6 +208,8 @@ impl<D> CheckPoint<D> | |
| where | ||
| D: ToBlockHash + fmt::Debug + Copy, | ||
| { | ||
| const MTP_BLOCK_COUNT: u32 = 11; | ||
|
|
||
| /// Construct a new base [`CheckPoint`] from given `height` and `data` at the front of a linked | ||
| /// list. | ||
| pub fn new(height: u32, data: D) -> Self { | ||
|
|
@@ -208,6 +223,54 @@ where | |
| })) | ||
| } | ||
|
|
||
| /// Calculate the median time past (MTP) for this checkpoint. | ||
| /// | ||
| /// Uses the 11 previous blocks (heights h-11 through h-1, where h is the current height) | ||
| /// to compute the MTP for the current block. This is used in Bitcoin's consensus rules | ||
| /// for time-based validations (BIP113). | ||
| /// | ||
| /// Note: This is a pseudo-median that doesn't average the two middle values. | ||
| /// | ||
| /// Returns `None` if the data type doesn't support block times or if any of the required | ||
| /// 11 sequential blocks are missing. | ||
| pub fn median_time_past(&self) -> Option<u32> { | ||
| let current_height = self.height(); | ||
| let earliest_height = current_height.saturating_sub(Self::MTP_BLOCK_COUNT); | ||
| self._median_time_past(earliest_height..current_height) | ||
| } | ||
|
Comment on lines
+236
to
+240
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm curious if these new APIs could output wrong/invalid values if the blocks inserted have a wrong/invalid time field e.g malicious block source.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Definitely. We are trusting the chain source here (and everywhere in BDK). Eventually, we should think about how to verify all consensus rules in BDK: difficulty, PoW, MTP, etc.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is it not inclusive of the current height?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: I want to avoid the temptation to resort to using these underscore methods. |
||
|
|
||
| /// Calculate the median time past (MTP) for the next block. | ||
| /// | ||
| /// Uses the 11 most recent blocks (heights h-10 through h, where h is the current height) | ||
| /// to compute what the MTP would be if a new block were added at height h+1. | ||
| /// | ||
| /// This differs from [`median_time_past`] which uses blocks h-11 through h-1 to compute | ||
| /// the MTP for the current block at height h. | ||
| /// | ||
| /// Returns `None` if the data type doesn't support block times or if any of the required | ||
| /// 11 sequential blocks are missing. | ||
| /// | ||
| /// [`median_time_past`]: CheckPoint::median_time_past | ||
| pub fn next_median_time_past(&self) -> Option<u32> { | ||
| let current_height = self.height(); | ||
| let earliest_height = current_height.saturating_sub(Self::MTP_BLOCK_COUNT - 1); | ||
| self._median_time_past(earliest_height..=current_height) | ||
| } | ||
|
|
||
| fn _median_time_past(&self, heights: impl IntoIterator<Item = u32>) -> Option<u32> { | ||
| let mut timestamps = heights | ||
| .into_iter() | ||
| .map(|height| { | ||
| // Return `None` for missing blocks or missing block times | ||
| let cp = self.get(height)?; | ||
| let block_time = cp.data_ref().block_time()?; | ||
| Some(block_time) | ||
| }) | ||
| .collect::<Option<Vec<u32>>>()?; | ||
| timestamps.sort_unstable(); | ||
| Some(timestamps[timestamps.len().checked_sub(1)? / 2]) | ||
| } | ||
|
|
||
| /// Construct from an iterator of block data. | ||
| /// | ||
| /// Returns `Err(None)` if `blocks` doesn't yield any data. If the blocks are not in ascending | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm but then the trait would need to be called
ToBlockHashAndHeaderTime? Maybe just have a new impl whereDis convertible to aHeader(granted, the time is only a subset of the information contained in a header).