From a3228d46362a5213280ede598425896f9cfe0f68 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Mon, 5 May 2025 21:46:32 +0000 Subject: [PATCH 01/10] units: pull u32 conversions for BlockHeight/BlockInterval into macro There is a lot of duplicated code between BlockHeight and BlockInterval. It obfuscates the differences between them: which timelock types they can be converted to/from and what their arithmetic properties are. --- units/src/block.rs | 167 +++++++++++++++++++-------------------------- 1 file changed, 69 insertions(+), 98 deletions(-) diff --git a/units/src/block.rs b/units/src/block.rs index 000f0995b..589cabcb5 100644 --- a/units/src/block.rs +++ b/units/src/block.rs @@ -22,46 +22,71 @@ use serde::{Deserialize, Serialize}; use crate::locktime; use crate::locktime::{absolute, relative}; -/// The block height, zero denotes the genesis block. -/// -/// This type is not meant for constructing height based timelocks, this is a general purpose block -/// height abstraction. For locktimes please see [`locktime::absolute::Height`]. -/// -/// This is a thin wrapper around a `u32` that may take on all values of a `u32`. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -// Public to try and make it really clear that there are no invariants. -pub struct BlockHeight(pub u32); +macro_rules! impl_u32_wrapper { + { + $(#[$($type_attrs:tt)*])* + $type_vis:vis struct $newtype:ident($inner_vis:vis u32); + } => { + $(#[$($type_attrs)*])* + $type_vis struct $newtype($inner_vis u32); -impl BlockHeight { - /// Block height 0, the genesis block. - pub const ZERO: Self = BlockHeight(0); + impl $newtype { + /// Block height 0, the genesis block. + pub const ZERO: Self = Self(0); - /// The minimum block height (0), the genesis block. - pub const MIN: Self = Self::ZERO; + /// The minimum block height (0), the genesis block. + pub const MIN: Self = Self::ZERO; - /// The maximum block height. - pub const MAX: Self = BlockHeight(u32::MAX); + /// The maximum block height. + pub const MAX: Self = Self(u32::MAX); - /// Constructs a new block height from a `u32`. - pub const fn from_u32(inner: u32) -> Self { Self(inner) } + /// Constructs a new block height from a `u32`. + pub const fn from_u32(inner: u32) -> Self { Self(inner) } - /// Returns block height as a `u32`. - pub const fn to_u32(self) -> u32 { self.0 } + /// Returns block height as a `u32`. + pub const fn to_u32(self) -> u32 { self.0 } + } + + impl fmt::Display for $newtype { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) } + } + + crate::impl_parse_str_from_int_infallible!($newtype, u32, from); + + impl From for $newtype { + fn from(inner: u32) -> Self { Self::from_u32(inner) } + } + + impl From<$newtype> for u32 { + fn from(height: $newtype) -> Self { height.to_u32() } + } + + #[cfg(feature = "arbitrary")] + impl<'a> Arbitrary<'a> for $newtype { + fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { + let choice = u.int_in_range(0..=2)?; + match choice { + 0 => Ok(Self::ZERO), + 1 => Ok(Self::MIN), + 2 => Ok(Self::MAX), + _ => Ok(Self::from_u32(u32::arbitrary(u)?)), + } + } + } + } } -impl fmt::Display for BlockHeight { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) } -} - -crate::impl_parse_str_from_int_infallible!(BlockHeight, u32, from); - -impl From for BlockHeight { - fn from(inner: u32) -> Self { Self::from_u32(inner) } -} - -impl From for u32 { - fn from(height: BlockHeight) -> Self { height.to_u32() } +impl_u32_wrapper! { + /// A block height. Zero denotes the genesis block. + /// + /// This type is not meant for constructing height based timelocks. It is a general purpose + /// blockheight abstraction. For locktimes please see [`locktime::absolute::Height`]. + /// + /// This is a thin wrapper around a `u32` that may take on all values of a `u32`. + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] + // Public to try and make it really clear that there are no invariants. + pub struct BlockHeight(pub u32); } impl From for BlockHeight { @@ -86,47 +111,17 @@ impl TryFrom for absolute::Height { } } -/// The block interval. -/// -/// Block interval is an integer type denoting the number of blocks that has passed since some point -/// i.e., this type is meant for usage as a relative block measure. -/// -/// This type is not meant for constructing relative height based timelocks, this is a general -/// purpose block interval abstraction. For locktimes please see [`locktime::relative::Height`]. -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -// Public to try and make it really clear that there are no invariants. -pub struct BlockInterval(pub u32); - -impl BlockInterval { - /// Block interval 0 i.e., the current block. - pub const ZERO: Self = BlockInterval(0); - - /// The minimum block interval (0). - pub const MIN: Self = Self::ZERO; - - /// The maximum block interval. - pub const MAX: Self = BlockInterval(u32::MAX); - - /// Constructs a new block interval from a `u32`. - pub const fn from_u32(inner: u32) -> Self { Self(inner) } - - /// Returns block interval as a `u32`. - pub const fn to_u32(self) -> u32 { self.0 } -} - -impl fmt::Display for BlockInterval { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) } -} - -crate::impl_parse_str_from_int_infallible!(BlockInterval, u32, from); - -impl From for BlockInterval { - fn from(inner: u32) -> Self { Self::from_u32(inner) } -} - -impl From for u32 { - fn from(height: BlockInterval) -> Self { height.to_u32() } +impl_u32_wrapper! { + /// An unsigned block interval. + /// + /// Block interval is an integer type representing a difference between the heights of two blocks. + /// + /// This type is not meant for constructing relative height based timelocks. It is a general + /// purpose block interval abstraction. For locktimes please see [`locktime::relative::Height`]. + #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] + // Public to try and make it really clear that there are no invariants. + pub struct BlockInterval(pub u32); } impl From for BlockInterval { @@ -244,30 +239,6 @@ impl<'a> core::iter::Sum<&'a BlockInterval> for BlockInterval { } } -#[cfg(feature = "arbitrary")] -impl<'a> Arbitrary<'a> for BlockHeight { - fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { - let choice = u.int_in_range(0..=2)?; - match choice { - 0 => Ok(BlockHeight::MIN), - 1 => Ok(BlockHeight::MAX), - _ => Ok(BlockHeight::from_u32(u32::arbitrary(u)?)), - } - } -} - -#[cfg(feature = "arbitrary")] -impl<'a> Arbitrary<'a> for BlockInterval { - fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { - let choice = u.int_in_range(0..=2)?; - match choice { - 0 => Ok(BlockInterval::MIN), - 1 => Ok(BlockInterval::MAX), - _ => Ok(BlockInterval::from_u32(u32::arbitrary(u)?)), - } - } -} - #[cfg(test)] mod tests { use super::*; From 4e3af5162f0ee5b1fa31cffea3e8ce501892ce8d Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Mon, 5 May 2025 22:08:32 +0000 Subject: [PATCH 02/10] units: add global `BlockMtp` type For our relative locktime API, we are going to want to take differences of arbitrary MTPs in order to check whether they meet some relative timelock threshold. However, the `locktime::absolute::Mtp` type can only represent MTPs that exceed 500 million. In practice this is a non-issue; by consensus MTPs must be monotonic and every real chain (even test chains) have initial real MTPs well above 500 million, which as a UNIX timestamp corresponds to November 5, 1985. But in theory this is a big problem: if we were to treat relative MTPs as "differences of absolute-timelock MTPs" then we will be unable to construct relative timelocks on chains with weird timestamps (and on legitimate chains, we'd have .unwrap()s everywhere that would be hard to justify). But we need to treat them as a "difference of MTPs" in *some* sense, because otherwise they'd be very hard to construct. --- bitcoin/src/lib.rs | 2 +- primitives/src/lib.rs | 2 +- units/src/block.rs | 49 +++++++++++++++++++++++++++++++++++++++---- units/src/lib.rs | 2 +- units/tests/api.rs | 8 +++++-- 5 files changed, 54 insertions(+), 9 deletions(-) diff --git a/bitcoin/src/lib.rs b/bitcoin/src/lib.rs index 4265be8db..9df6b25f5 100644 --- a/bitcoin/src/lib.rs +++ b/bitcoin/src/lib.rs @@ -134,7 +134,7 @@ pub use primitives::{ #[doc(inline)] pub use units::{ amount::{Amount, Denomination, SignedAmount}, - block::{BlockHeight, BlockInterval}, + block::{BlockHeight, BlockInterval, BlockMtp}, fee_rate::FeeRate, time::{self, BlockTime}, weight::Weight, diff --git a/primitives/src/lib.rs b/primitives/src/lib.rs index f9dafc05a..f47749c01 100644 --- a/primitives/src/lib.rs +++ b/primitives/src/lib.rs @@ -50,7 +50,7 @@ pub mod witness; #[doc(inline)] pub use units::{ amount::{self, Amount, SignedAmount}, - block::{BlockHeight, BlockInterval}, + block::{BlockHeight, BlockInterval, BlockMtp}, fee_rate::{self, FeeRate}, time::{self, BlockTime}, weight::{self, Weight}, diff --git a/units/src/block.rs b/units/src/block.rs index 589cabcb5..d3fdf66ee 100644 --- a/units/src/block.rs +++ b/units/src/block.rs @@ -93,8 +93,7 @@ impl From for BlockHeight { /// Converts a [`locktime::absolute::Height`] to a [`BlockHeight`]. /// /// An absolute locktime block height has a maximum value of [`absolute::LOCK_TIME_THRESHOLD`] - /// (500,000,000) where as a [`BlockHeight`] is a thin wrapper around a `u32`, the two types are - /// not interchangeable. + /// minus one, while [`BlockHeight`] may take the full range of `u32`. fn from(h: absolute::Height) -> Self { Self::from_u32(h.to_u32()) } } @@ -104,8 +103,7 @@ impl TryFrom for absolute::Height { /// Converts a [`BlockHeight`] to a [`locktime::absolute::Height`]. /// /// An absolute locktime block height has a maximum value of [`absolute::LOCK_TIME_THRESHOLD`] - /// (500,000,000) where as a [`BlockHeight`] is a thin wrapper around a `u32`, the two types are - /// not interchangeable. + /// minus one, while [`BlockHeight`] may take the full range of `u32`. fn try_from(h: BlockHeight) -> Result { absolute::Height::from_u32(h.to_u32()) } @@ -149,6 +147,49 @@ impl TryFrom for relative::HeightInterval { } } +impl_u32_wrapper! { + /// The median timestamp of 11 consecutive blocks. + /// + /// This type is not meant for constructing time-based timelocks. It is a general purpose + /// MTP abstraction. For locktimes please see [`locktime::absolute::Mtp`]. + /// + /// This is a thin wrapper around a `u32` that may take on all values of a `u32`. + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] + // Public to try and make it really clear that there are no invariants. + pub struct BlockMtp(pub u32); +} + +impl BlockMtp { + /// Constructs a [`BlockMtp`] by computing the median‐time‐past from the last 11 block timestamps + /// + /// Because block timestamps are not monotonic, this function internally sorts them; + /// it is therefore not important what order they appear in the array; use whatever + /// is most convenient. + pub fn new(mut timestamps: [crate::BlockTime; 11]) -> Self { + timestamps.sort_unstable(); + Self::from_u32(u32::from(timestamps[5])) + } +} + +impl From for BlockMtp { + /// Converts a [`locktime::absolute::Mtp`] to a [`BlockMtp`]. + /// + /// An absolute locktime MTP has a minimum value of [`absolute::LOCK_TIME_THRESHOLD`], + /// while [`BlockMtp`] may take the full range of `u32`. + fn from(h: absolute::Mtp) -> Self { Self::from_u32(h.to_u32()) } +} + +impl TryFrom for absolute::Mtp { + type Error = absolute::ConversionError; + + /// Converts a [`BlockHeight`] to a [`locktime::absolute::Height`]. + /// + /// An absolute locktime MTP has a minimum value of [`absolute::LOCK_TIME_THRESHOLD`], + /// while [`BlockMtp`] may take the full range of `u32`. + fn try_from(h: BlockMtp) -> Result { absolute::Mtp::from_u32(h.to_u32()) } +} + /// Error returned when the block interval is too big to be used as a relative lock time. #[derive(Debug, Clone, PartialEq, Eq)] pub struct TooBigForRelativeBlockHeightIntervalError(u32); diff --git a/units/src/lib.rs b/units/src/lib.rs index 41c33b017..f559592ab 100644 --- a/units/src/lib.rs +++ b/units/src/lib.rs @@ -57,7 +57,7 @@ pub mod weight; #[rustfmt::skip] pub use self::{ amount::{Amount, SignedAmount}, - block::{BlockHeight, BlockInterval}, + block::{BlockHeight, BlockInterval, BlockMtp}, fee_rate::FeeRate, result::{NumOpError, NumOpResult, MathOp}, time::BlockTime, diff --git a/units/tests/api.rs b/units/tests/api.rs index 41b39bb4b..94913229b 100644 --- a/units/tests/api.rs +++ b/units/tests/api.rs @@ -14,7 +14,7 @@ use arbitrary::{Arbitrary, Unstructured}; // These imports test "typical" usage by user code. use bitcoin_units::locktime::{absolute, relative}; // Typical usage is `absolute::Height`. use bitcoin_units::{ - amount, block, fee_rate, locktime, parse, weight, Amount, BlockHeight, BlockInterval, + amount, block, fee_rate, locktime, parse, weight, Amount, BlockHeight, BlockInterval, BlockMtp, BlockTime, FeeRate, SignedAmount, Weight, }; @@ -43,6 +43,7 @@ struct Structs { j: relative::Time, k: Weight, l: BlockTime, + m: BlockMtp, } impl Structs { @@ -60,6 +61,7 @@ impl Structs { j: relative::Time::MAX, k: Weight::MAX, l: BlockTime::from_u32(u32::MAX), + m: BlockMtp::MAX, } } } @@ -90,6 +92,7 @@ struct CommonTraits { j: relative::Time, k: Weight, l: BlockTime, + m: BlockMtp, } /// A struct that includes all types that implement `Default`. @@ -147,7 +150,7 @@ fn api_can_use_modules_from_crate_root() { #[test] fn api_can_use_types_from_crate_root() { use bitcoin_units::{ - Amount, BlockHeight, BlockInterval, BlockTime, FeeRate, SignedAmount, Weight, + Amount, BlockHeight, BlockInterval, BlockMtp, BlockTime, FeeRate, SignedAmount, Weight, }; } @@ -298,6 +301,7 @@ impl<'a> Arbitrary<'a> for Structs { j: relative::Time::arbitrary(u)?, k: Weight::arbitrary(u)?, l: BlockTime::arbitrary(u)?, + m: BlockMtp::arbitrary(u)?, }; Ok(a) } From cb882c5ce1fe6fa3e67142b89a9050f6e832be4b Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Mon, 5 May 2025 22:19:29 +0000 Subject: [PATCH 03/10] units: add global `BlockMtpInterval` type See the previous commit message for justification; for sensible arithmetic on block timestamps we need the ability to do MTP calculations on arbitrary MTPs and arbitrary intervals between them. However, the absolute::Mtp and relative::MtpInterval types are severely limited in both range and precision. Also adds a bunch of arithmetic ops to match the existing ops for BlockHeight and BlockInterval. These panic on overflow, just like the underlying std arithmetic, which I think is reasonable behavior for types which are documented as being thin wrappers around u32. We may want to add checked_add, checked_sub and maybe checked_sum methods, but that's out of scope for this PR. --- units/src/block.rs | 125 +++++++++++++++++++++++++++++++++ units/src/lib.rs | 2 +- units/src/locktime/relative.rs | 6 ++ units/tests/api.rs | 11 ++- 4 files changed, 141 insertions(+), 3 deletions(-) diff --git a/units/src/block.rs b/units/src/block.rs index d3fdf66ee..82f2e6bf8 100644 --- a/units/src/block.rs +++ b/units/src/block.rs @@ -190,6 +190,62 @@ impl TryFrom for absolute::Mtp { fn try_from(h: BlockMtp) -> Result { absolute::Mtp::from_u32(h.to_u32()) } } +impl_u32_wrapper! { + /// An unsigned difference between two [`BlockMtp`]s. + /// + /// This type is not meant for constructing time-based timelocks. It is a general purpose + /// MTP abstraction. For locktimes please see [`locktime::relative::MtpInterval`]. + /// + /// This is a thin wrapper around a `u32` that may take on all values of a `u32`. + #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] + // Public to try and make it really clear that there are no invariants. + pub struct BlockMtpInterval(pub u32); +} + +impl BlockMtpInterval { + /// Converts a [`BlockMtpInterval`] to a [`locktime::relative::MtpInterval`], rounding down. + /// + /// Relative timelock MTP intervals have a resolution of 512 seconds, while + /// [`BlockMtpInterval`], like all block timestamp types, has a one-second resolution. + /// + /// # Errors + /// + /// Errors if the MTP is out-of-range (in excess of 512 times `u16::MAX` seconds, or about + /// 388 days) for a time-based relative locktime. + #[inline] + pub const fn to_relative_mtp_interval_floor( + self, + ) -> Result { + relative::MtpInterval::from_seconds_floor(self.to_u32()) + } + + /// Converts a [`BlockMtpInterval`] to a [`locktime::relative::MtpInterval`], rounding up. + /// + /// Relative timelock MTP intervals have a resolution of 512 seconds, while + /// [`BlockMtpInterval`], like all block timestamp types, has a one-second resolution. + /// + /// # Errors + /// + /// Errors if the MTP is out-of-range (in excess of 512 times `u16::MAX` seconds, or about + /// 388 days) for a time-based relative locktime. + #[inline] + pub const fn to_relative_mtp_interval_ceil( + self, + ) -> Result { + relative::MtpInterval::from_seconds_ceil(self.to_u32()) + } +} + +impl From for BlockMtpInterval { + /// Converts a [`locktime::relative::MtpInterval`] to a [`BlockMtpInterval `]. + /// + /// A relative locktime MTP interval has a resolution of 512 seconds, and a maximum value + /// of `u16::MAX` 512-second intervals. [`BlockMtpInterval`] may take the full range of + /// `u32`. + fn from(h: relative::MtpInterval) -> Self { Self::from_u32(h.to_seconds()) } +} + /// Error returned when the block interval is too big to be used as a relative lock time. #[derive(Debug, Clone, PartialEq, Eq)] pub struct TooBigForRelativeBlockHeightIntervalError(u32); @@ -258,10 +314,62 @@ crate::internal_macros::impl_op_for_references! { BlockInterval::from_u32(height) } } + + // height - height = interval + impl ops::Sub for BlockMtp { + type Output = BlockMtpInterval; + + fn sub(self, rhs: BlockMtp) -> Self::Output { + let interval = self.to_u32() - rhs.to_u32(); + BlockMtpInterval::from_u32(interval) + } + } + + // height + interval = height + impl ops::Add for BlockMtp { + type Output = BlockMtp; + + fn add(self, rhs: BlockMtpInterval) -> Self::Output { + let height = self.to_u32() + rhs.to_u32(); + BlockMtp::from_u32(height) + } + } + + // height - interval = height + impl ops::Sub for BlockMtp { + type Output = BlockMtp; + + fn sub(self, rhs: BlockMtpInterval) -> Self::Output { + let height = self.to_u32() - rhs.to_u32(); + BlockMtp::from_u32(height) + } + } + + // interval + interval = interval + impl ops::Add for BlockMtpInterval { + type Output = BlockMtpInterval; + + fn add(self, rhs: BlockMtpInterval) -> Self::Output { + let height = self.to_u32() + rhs.to_u32(); + BlockMtpInterval::from_u32(height) + } + } + + // interval - interval = interval + impl ops::Sub for BlockMtpInterval { + type Output = BlockMtpInterval; + + fn sub(self, rhs: BlockMtpInterval) -> Self::Output { + let height = self.to_u32() - rhs.to_u32(); + BlockMtpInterval::from_u32(height) + } + } } crate::internal_macros::impl_add_assign!(BlockInterval); crate::internal_macros::impl_sub_assign!(BlockInterval); +crate::internal_macros::impl_add_assign!(BlockMtpInterval); +crate::internal_macros::impl_sub_assign!(BlockMtpInterval); impl core::iter::Sum for BlockInterval { fn sum>(iter: I) -> Self { @@ -280,6 +388,23 @@ impl<'a> core::iter::Sum<&'a BlockInterval> for BlockInterval { } } +impl core::iter::Sum for BlockMtpInterval { + fn sum>(iter: I) -> Self { + let sum = iter.map(|interval| interval.0).sum(); + BlockMtpInterval::from_u32(sum) + } +} + +impl<'a> core::iter::Sum<&'a BlockMtpInterval> for BlockMtpInterval { + fn sum(iter: I) -> Self + where + I: Iterator, + { + let sum = iter.map(|interval| interval.0).sum(); + BlockMtpInterval::from_u32(sum) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/units/src/lib.rs b/units/src/lib.rs index f559592ab..990a0a9cb 100644 --- a/units/src/lib.rs +++ b/units/src/lib.rs @@ -57,7 +57,7 @@ pub mod weight; #[rustfmt::skip] pub use self::{ amount::{Amount, SignedAmount}, - block::{BlockHeight, BlockInterval, BlockMtp}, + block::{BlockHeight, BlockInterval, BlockMtp, BlockMtpInterval}, fee_rate::FeeRate, result::{NumOpError, NumOpResult, MathOp}, time::BlockTime, diff --git a/units/src/locktime/relative.rs b/units/src/locktime/relative.rs index 47dde70aa..a98b11fb7 100644 --- a/units/src/locktime/relative.rs +++ b/units/src/locktime/relative.rs @@ -154,6 +154,12 @@ impl MtpInterval { } } + /// Represents the [`MtpInterval`] as an integer number of seconds. + #[inline] + pub const fn to_seconds(self) -> u32 { + self.0 as u32 * 512 // u16->u32 cast ok, const context + } + /// Returns the inner `u16` value. #[inline] #[must_use] diff --git a/units/tests/api.rs b/units/tests/api.rs index 94913229b..69615aef4 100644 --- a/units/tests/api.rs +++ b/units/tests/api.rs @@ -15,7 +15,7 @@ use arbitrary::{Arbitrary, Unstructured}; use bitcoin_units::locktime::{absolute, relative}; // Typical usage is `absolute::Height`. use bitcoin_units::{ amount, block, fee_rate, locktime, parse, weight, Amount, BlockHeight, BlockInterval, BlockMtp, - BlockTime, FeeRate, SignedAmount, Weight, + BlockMtpInterval, BlockTime, FeeRate, SignedAmount, Weight, }; /// A struct that includes all public non-error enums. @@ -44,6 +44,7 @@ struct Structs { k: Weight, l: BlockTime, m: BlockMtp, + n: BlockMtpInterval, } impl Structs { @@ -62,6 +63,7 @@ impl Structs { k: Weight::MAX, l: BlockTime::from_u32(u32::MAX), m: BlockMtp::MAX, + n: BlockMtpInterval::MAX, } } } @@ -93,6 +95,7 @@ struct CommonTraits { k: Weight, l: BlockTime, m: BlockMtp, + n: BlockMtpInterval, } /// A struct that includes all types that implement `Default`. @@ -103,6 +106,7 @@ struct Default { c: BlockInterval, d: relative::Height, e: relative::Time, + f: BlockMtpInterval, } /// A struct that includes all public error types. @@ -150,7 +154,8 @@ fn api_can_use_modules_from_crate_root() { #[test] fn api_can_use_types_from_crate_root() { use bitcoin_units::{ - Amount, BlockHeight, BlockInterval, BlockMtp, BlockTime, FeeRate, SignedAmount, Weight, + Amount, BlockHeight, BlockInterval, BlockMtp, BlockMtpInterval, BlockTime, FeeRate, + SignedAmount, Weight, }; } @@ -260,6 +265,7 @@ fn regression_default() { c: BlockInterval::ZERO, d: relative::Height::ZERO, e: relative::Time::ZERO, + f: BlockMtpInterval::ZERO, }; assert_eq!(got, want); } @@ -302,6 +308,7 @@ impl<'a> Arbitrary<'a> for Structs { k: Weight::arbitrary(u)?, l: BlockTime::arbitrary(u)?, m: BlockMtp::arbitrary(u)?, + n: BlockMtpInterval::arbitrary(u)?, }; Ok(a) } From 4e4601b3d5de9cce461ad10298ece1f78bc7ee47 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Mon, 5 May 2025 22:42:35 +0000 Subject: [PATCH 04/10] units: rename BlockInterval to BlockHeightInterval Now that we have BlockMtpInterval we want to distinguish BlockInterval from it. --- bitcoin/src/blockdata/block.rs | 8 ++- bitcoin/src/lib.rs | 10 +++- bitcoin/src/network/params.rs | 30 +++++----- primitives/src/lib.rs | 6 +- units/src/block.rs | 102 +++++++++++++++++---------------- units/src/lib.rs | 6 +- units/tests/api.rs | 2 +- 7 files changed, 93 insertions(+), 71 deletions(-) diff --git a/bitcoin/src/blockdata/block.rs b/bitcoin/src/blockdata/block.rs index 5fd59aa31..3f2b86fa7 100644 --- a/bitcoin/src/blockdata/block.rs +++ b/bitcoin/src/blockdata/block.rs @@ -30,7 +30,13 @@ use crate::transaction::{Transaction, TransactionExt as _, Wtxid}; #[doc(inline)] pub use primitives::block::{Block, Checked, Unchecked, Validation, Version, BlockHash, Header, WitnessCommitment}; #[doc(inline)] -pub use units::block::{BlockHeight, BlockInterval, TooBigForRelativeBlockHeightIntervalError}; +pub use units::block::{ + BlockHeight, BlockHeightInterval, TooBigForRelativeBlockHeightIntervalError, +}; + +#[deprecated(since = "TBD", note = "use `BlockHeightInterval` instead")] +#[doc(hidden)] +pub type BlockInterval = BlockHeightInterval; impl_hashencode!(BlockHash); diff --git a/bitcoin/src/lib.rs b/bitcoin/src/lib.rs index 9df6b25f5..0bcbbe209 100644 --- a/bitcoin/src/lib.rs +++ b/bitcoin/src/lib.rs @@ -134,12 +134,16 @@ pub use primitives::{ #[doc(inline)] pub use units::{ amount::{Amount, Denomination, SignedAmount}, - block::{BlockHeight, BlockInterval, BlockMtp}, + block::{BlockHeight, BlockHeightInterval, BlockMtp}, fee_rate::FeeRate, time::{self, BlockTime}, weight::Weight, }; +#[deprecated(since = "TBD", note = "use `BlockHeightInterval` instead")] +#[doc(hidden)] +pub type BlockInterval = BlockHeightInterval; + #[doc(inline)] pub use crate::{ address::{Address, AddressType, KnownHrp}, @@ -242,7 +246,7 @@ mod encode_impls { //! Encodable/Decodable implementations. // While we are deprecating, re-exporting, and generally moving things around just put these here. - use units::{BlockHeight, BlockInterval}; + use units::{BlockHeight, BlockHeightInterval}; use crate::consensus::{encode, Decodable, Encodable}; use crate::io::{BufRead, Write}; @@ -274,5 +278,5 @@ mod encode_impls { } impl_encodable_for_u32_wrapper!(BlockHeight); - impl_encodable_for_u32_wrapper!(BlockInterval); + impl_encodable_for_u32_wrapper!(BlockHeightInterval); } diff --git a/bitcoin/src/network/params.rs b/bitcoin/src/network/params.rs index 35f68bfb0..493354e69 100644 --- a/bitcoin/src/network/params.rs +++ b/bitcoin/src/network/params.rs @@ -67,7 +67,7 @@ //! # } //! ``` -use units::{BlockHeight, BlockInterval}; +use units::{BlockHeight, BlockHeightInterval}; use crate::network::Network; #[cfg(doc)] @@ -92,9 +92,9 @@ pub struct Params { /// Minimum blocks including miner confirmation of the total of 2016 blocks in a retargeting period, /// (nPowTargetTimespan / nPowTargetSpacing) which is also used for BIP9 deployments. /// Examples: 1916 for 95%, 1512 for testchains. - pub rule_change_activation_threshold: BlockInterval, + pub rule_change_activation_threshold: BlockHeightInterval, /// Number of blocks with the same set of rules. - pub miner_confirmation_window: BlockInterval, + pub miner_confirmation_window: BlockHeightInterval, /// Proof of work limit value. It contains the lowest possible difficulty. #[deprecated(since = "0.32.0", note = "use `max_attainable_target` instead")] pub pow_limit: Target, @@ -152,8 +152,8 @@ impl Params { bip34_height: BlockHeight::from_u32(227931), // 000000000000024b89b42a942fe0d9fea3bb44ab7bd1b19115dd6a759c0808b8 bip65_height: BlockHeight::from_u32(388381), // 000000000000000004c2b624ed5d7756c508d90fd0da2c7c679febfa6c4735f0 bip66_height: BlockHeight::from_u32(363725), // 00000000000000000379eaa19dce8c9b722d46ae6a57c2f1a988119488b50931 - rule_change_activation_threshold: BlockInterval::from_u32(1916), // 95% - miner_confirmation_window: BlockInterval::from_u32(2016), + rule_change_activation_threshold: BlockHeightInterval::from_u32(1916), // 95% + miner_confirmation_window: BlockHeightInterval::from_u32(2016), pow_limit: Target::MAX_ATTAINABLE_MAINNET, max_attainable_target: Target::MAX_ATTAINABLE_MAINNET, pow_target_spacing: 10 * 60, // 10 minutes. @@ -170,8 +170,8 @@ impl Params { bip34_height: BlockHeight::from_u32(21111), // 0000000023b3a96d3484e5abb3755c413e7d41500f8e2a5c3f0dd01299cd8ef8 bip65_height: BlockHeight::from_u32(581885), // 00000000007f6655f22f98e72ed80d8b06dc761d5da09df0fa1dc4be4f861eb6 bip66_height: BlockHeight::from_u32(330776), // 000000002104c8c45e99a8853285a3b592602a3ccde2b832481da85e9e4ba182 - rule_change_activation_threshold: BlockInterval::from_u32(1512), // 75% - miner_confirmation_window: BlockInterval::from_u32(2016), + rule_change_activation_threshold: BlockHeightInterval::from_u32(1512), // 75% + miner_confirmation_window: BlockHeightInterval::from_u32(2016), pow_limit: Target::MAX_ATTAINABLE_TESTNET, max_attainable_target: Target::MAX_ATTAINABLE_TESTNET, pow_target_spacing: 10 * 60, // 10 minutes. @@ -187,8 +187,8 @@ impl Params { bip34_height: BlockHeight::from_u32(21111), // 0000000023b3a96d3484e5abb3755c413e7d41500f8e2a5c3f0dd01299cd8ef8 bip65_height: BlockHeight::from_u32(581885), // 00000000007f6655f22f98e72ed80d8b06dc761d5da09df0fa1dc4be4f861eb6 bip66_height: BlockHeight::from_u32(330776), // 000000002104c8c45e99a8853285a3b592602a3ccde2b832481da85e9e4ba182 - rule_change_activation_threshold: BlockInterval::from_u32(1512), // 75% - miner_confirmation_window: BlockInterval::from_u32(2016), + rule_change_activation_threshold: BlockHeightInterval::from_u32(1512), // 75% + miner_confirmation_window: BlockHeightInterval::from_u32(2016), pow_limit: Target::MAX_ATTAINABLE_TESTNET, max_attainable_target: Target::MAX_ATTAINABLE_TESTNET, pow_target_spacing: 10 * 60, // 10 minutes. @@ -204,8 +204,8 @@ impl Params { bip34_height: BlockHeight::from_u32(1), bip65_height: BlockHeight::from_u32(1), bip66_height: BlockHeight::from_u32(1), - rule_change_activation_threshold: BlockInterval::from_u32(1512), // 75% - miner_confirmation_window: BlockInterval::from_u32(2016), + rule_change_activation_threshold: BlockHeightInterval::from_u32(1512), // 75% + miner_confirmation_window: BlockHeightInterval::from_u32(2016), pow_limit: Target::MAX_ATTAINABLE_TESTNET, max_attainable_target: Target::MAX_ATTAINABLE_TESTNET, pow_target_spacing: 10 * 60, // 10 minutes. @@ -221,8 +221,8 @@ impl Params { bip34_height: BlockHeight::from_u32(1), bip65_height: BlockHeight::from_u32(1), bip66_height: BlockHeight::from_u32(1), - rule_change_activation_threshold: BlockInterval::from_u32(1916), // 95% - miner_confirmation_window: BlockInterval::from_u32(2016), + rule_change_activation_threshold: BlockHeightInterval::from_u32(1916), // 95% + miner_confirmation_window: BlockHeightInterval::from_u32(2016), pow_limit: Target::MAX_ATTAINABLE_SIGNET, max_attainable_target: Target::MAX_ATTAINABLE_SIGNET, pow_target_spacing: 10 * 60, // 10 minutes. @@ -238,8 +238,8 @@ impl Params { bip34_height: BlockHeight::from_u32(100000000), // not activated on regtest bip65_height: BlockHeight::from_u32(1351), bip66_height: BlockHeight::from_u32(1251), // used only in rpc tests - rule_change_activation_threshold: BlockInterval::from_u32(108), // 75% - miner_confirmation_window: BlockInterval::from_u32(144), + rule_change_activation_threshold: BlockHeightInterval::from_u32(108), // 75% + miner_confirmation_window: BlockHeightInterval::from_u32(144), pow_limit: Target::MAX_ATTAINABLE_REGTEST, max_attainable_target: Target::MAX_ATTAINABLE_REGTEST, pow_target_spacing: 10 * 60, // 10 minutes. diff --git a/primitives/src/lib.rs b/primitives/src/lib.rs index f47749c01..4a9da6e68 100644 --- a/primitives/src/lib.rs +++ b/primitives/src/lib.rs @@ -50,12 +50,16 @@ pub mod witness; #[doc(inline)] pub use units::{ amount::{self, Amount, SignedAmount}, - block::{BlockHeight, BlockInterval, BlockMtp}, + block::{BlockHeight, BlockHeightInterval, BlockMtp, BlockMtpInterval}, fee_rate::{self, FeeRate}, time::{self, BlockTime}, weight::{self, Weight}, }; +#[deprecated(since = "TBD", note = "use `BlockHeightInterval` instead")] +#[doc(hidden)] +pub type BlockInterval = BlockHeightInterval; + #[doc(inline)] #[cfg(feature = "alloc")] pub use self::{ diff --git a/units/src/block.rs b/units/src/block.rs index 82f2e6bf8..c94c9a3ab 100644 --- a/units/src/block.rs +++ b/units/src/block.rs @@ -119,25 +119,25 @@ impl_u32_wrapper! { #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] // Public to try and make it really clear that there are no invariants. - pub struct BlockInterval(pub u32); + pub struct BlockHeightInterval(pub u32); } -impl From for BlockInterval { - /// Converts a [`locktime::relative::HeightInterval`] to a [`BlockInterval`]. +impl From for BlockHeightInterval { + /// Converts a [`locktime::relative::HeightInterval`] to a [`BlockHeightInterval`]. /// /// A relative locktime block height has a maximum value of `u16::MAX` where as a - /// [`BlockInterval`] is a thin wrapper around a `u32`, the two types are not interchangeable. + /// [`BlockHeightInterval`] is a thin wrapper around a `u32`, the two types are not interchangeable. fn from(h: relative::HeightInterval) -> Self { Self::from_u32(h.to_height().into()) } } -impl TryFrom for relative::HeightInterval { +impl TryFrom for relative::HeightInterval { type Error = TooBigForRelativeBlockHeightIntervalError; - /// Converts a [`BlockInterval`] to a [`locktime::relative::HeightInterval`]. + /// Converts a [`BlockHeightInterval`] to a [`locktime::relative::HeightInterval`]. /// /// A relative locktime block height has a maximum value of `u16::MAX` where as a - /// [`BlockInterval`] is a thin wrapper around a `u32`, the two types are not interchangeable. - fn try_from(h: BlockInterval) -> Result { + /// [`BlockHeightInterval`] is a thin wrapper around a `u32`, the two types are not interchangeable. + fn try_from(h: BlockHeightInterval) -> Result { let h = h.to_u32(); if h > u32::from(u16::MAX) { @@ -267,51 +267,51 @@ impl std::error::Error for TooBigForRelativeBlockHeightIntervalError {} crate::internal_macros::impl_op_for_references! { // height - height = interval impl ops::Sub for BlockHeight { - type Output = BlockInterval; + type Output = BlockHeightInterval; fn sub(self, rhs: BlockHeight) -> Self::Output { let interval = self.to_u32() - rhs.to_u32(); - BlockInterval::from_u32(interval) + BlockHeightInterval::from_u32(interval) } } // height + interval = height - impl ops::Add for BlockHeight { + impl ops::Add for BlockHeight { type Output = BlockHeight; - fn add(self, rhs: BlockInterval) -> Self::Output { + fn add(self, rhs: BlockHeightInterval) -> Self::Output { let height = self.to_u32() + rhs.to_u32(); BlockHeight::from_u32(height) } } // height - interval = height - impl ops::Sub for BlockHeight { + impl ops::Sub for BlockHeight { type Output = BlockHeight; - fn sub(self, rhs: BlockInterval) -> Self::Output { + fn sub(self, rhs: BlockHeightInterval) -> Self::Output { let height = self.to_u32() - rhs.to_u32(); BlockHeight::from_u32(height) } } // interval + interval = interval - impl ops::Add for BlockInterval { - type Output = BlockInterval; + impl ops::Add for BlockHeightInterval { + type Output = BlockHeightInterval; - fn add(self, rhs: BlockInterval) -> Self::Output { + fn add(self, rhs: BlockHeightInterval) -> Self::Output { let height = self.to_u32() + rhs.to_u32(); - BlockInterval::from_u32(height) + BlockHeightInterval::from_u32(height) } } // interval - interval = interval - impl ops::Sub for BlockInterval { - type Output = BlockInterval; + impl ops::Sub for BlockHeightInterval { + type Output = BlockHeightInterval; - fn sub(self, rhs: BlockInterval) -> Self::Output { + fn sub(self, rhs: BlockHeightInterval) -> Self::Output { let height = self.to_u32() - rhs.to_u32(); - BlockInterval::from_u32(height) + BlockHeightInterval::from_u32(height) } } @@ -366,25 +366,25 @@ crate::internal_macros::impl_op_for_references! { } } -crate::internal_macros::impl_add_assign!(BlockInterval); -crate::internal_macros::impl_sub_assign!(BlockInterval); +crate::internal_macros::impl_add_assign!(BlockHeightInterval); +crate::internal_macros::impl_sub_assign!(BlockHeightInterval); crate::internal_macros::impl_add_assign!(BlockMtpInterval); crate::internal_macros::impl_sub_assign!(BlockMtpInterval); -impl core::iter::Sum for BlockInterval { +impl core::iter::Sum for BlockHeightInterval { fn sum>(iter: I) -> Self { let sum = iter.map(|interval| interval.0).sum(); - BlockInterval::from_u32(sum) + BlockHeightInterval::from_u32(sum) } } -impl<'a> core::iter::Sum<&'a BlockInterval> for BlockInterval { +impl<'a> core::iter::Sum<&'a BlockHeightInterval> for BlockHeightInterval { fn sum(iter: I) -> Self where - I: Iterator, + I: Iterator, { let sum = iter.map(|interval| interval.0).sum(); - BlockInterval::from_u32(sum) + BlockHeightInterval::from_u32(sum) } } @@ -414,17 +414,19 @@ mod tests { let height: u32 = BlockHeight(100).into(); assert_eq!(height, 100); - let interval: u32 = BlockInterval(100).into(); + let interval: u32 = BlockHeightInterval(100).into(); assert_eq!(interval, 100); - let interval_from_height: BlockInterval = relative::HeightInterval::from(10u16).into(); + let interval_from_height: BlockHeightInterval = + relative::HeightInterval::from(10u16).into(); assert_eq!(interval_from_height.to_u32(), 10u32); let invalid_height_greater = - relative::HeightInterval::try_from(BlockInterval(u32::from(u16::MAX) + 1)); + relative::HeightInterval::try_from(BlockHeightInterval(u32::from(u16::MAX) + 1)); assert!(invalid_height_greater.is_err()); - let valid_height = relative::HeightInterval::try_from(BlockInterval(u32::from(u16::MAX))); + let valid_height = + relative::HeightInterval::try_from(BlockHeightInterval(u32::from(u16::MAX))); assert!(valid_height.is_ok()); } @@ -432,39 +434,41 @@ mod tests { #[test] fn all_available_ops() { // height - height = interval - assert!(BlockHeight(10) - BlockHeight(7) == BlockInterval(3)); + assert!(BlockHeight(10) - BlockHeight(7) == BlockHeightInterval(3)); // height + interval = height - assert!(BlockHeight(100) + BlockInterval(1) == BlockHeight(101)); + assert!(BlockHeight(100) + BlockHeightInterval(1) == BlockHeight(101)); // height - interval == height - assert!(BlockHeight(100) - BlockInterval(1) == BlockHeight(99)); + assert!(BlockHeight(100) - BlockHeightInterval(1) == BlockHeight(99)); // interval + interval = interval - assert!(BlockInterval(1) + BlockInterval(2) == BlockInterval(3)); + assert!(BlockHeightInterval(1) + BlockHeightInterval(2) == BlockHeightInterval(3)); // interval - interval = interval - assert!(BlockInterval(10) - BlockInterval(7) == BlockInterval(3)); + assert!(BlockHeightInterval(10) - BlockHeightInterval(7) == BlockHeightInterval(3)); assert!( - [BlockInterval(1), BlockInterval(2), BlockInterval(3)].iter().sum::() - == BlockInterval(6) + [BlockHeightInterval(1), BlockHeightInterval(2), BlockHeightInterval(3)] + .iter() + .sum::() + == BlockHeightInterval(6) ); assert!( - [BlockInterval(4), BlockInterval(5), BlockInterval(6)] + [BlockHeightInterval(4), BlockHeightInterval(5), BlockHeightInterval(6)] .into_iter() - .sum::() - == BlockInterval(15) + .sum::() + == BlockHeightInterval(15) ); // interval += interval - let mut int = BlockInterval(1); - int += BlockInterval(2); - assert_eq!(int, BlockInterval(3)); + let mut int = BlockHeightInterval(1); + int += BlockHeightInterval(2); + assert_eq!(int, BlockHeightInterval(3)); // interval -= interval - let mut int = BlockInterval(10); - int -= BlockInterval(7); - assert_eq!(int, BlockInterval(3)); + let mut int = BlockHeightInterval(10); + int -= BlockHeightInterval(7); + assert_eq!(int, BlockHeightInterval(3)); } } diff --git a/units/src/lib.rs b/units/src/lib.rs index 990a0a9cb..e5f5dda25 100644 --- a/units/src/lib.rs +++ b/units/src/lib.rs @@ -57,10 +57,14 @@ pub mod weight; #[rustfmt::skip] pub use self::{ amount::{Amount, SignedAmount}, - block::{BlockHeight, BlockInterval, BlockMtp, BlockMtpInterval}, + block::{BlockHeight, BlockHeightInterval, BlockMtp, BlockMtpInterval}, fee_rate::FeeRate, result::{NumOpError, NumOpResult, MathOp}, time::BlockTime, weight::Weight }; pub(crate) use self::result::OptionExt; + +#[deprecated(since = "TBD", note = "use `BlockHeightInterval` instead")] +#[doc(hidden)] +pub type BlockInterval = BlockHeightInterval; diff --git a/units/tests/api.rs b/units/tests/api.rs index 69615aef4..a1956d10a 100644 --- a/units/tests/api.rs +++ b/units/tests/api.rs @@ -172,7 +172,7 @@ fn api_can_use_all_types_from_module_amount() { #[test] fn api_can_use_all_types_from_module_block() { use bitcoin_units::block::{ - BlockHeight, BlockInterval, TooBigForRelativeBlockHeightIntervalError, + BlockHeight, BlockHeightInterval, TooBigForRelativeBlockHeightIntervalError, }; } From 4300271f0c36716b89314159d7ae0eef28e5aa7f Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Mon, 5 May 2025 17:58:46 +0000 Subject: [PATCH 05/10] units: add constructor for absolute::Mtp from timestamps This is a convenience method to allow direct construction of absolute::Mtp rather than going through units::BlockMtp. --- units/src/locktime/absolute.rs | 44 ++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/units/src/locktime/absolute.rs b/units/src/locktime/absolute.rs index 1df4b04cf..214d048a1 100644 --- a/units/src/locktime/absolute.rs +++ b/units/src/locktime/absolute.rs @@ -154,6 +154,22 @@ impl Mtp { /// The maximum MTP allowable in a locktime (Sun Feb 07 2106 06:28:15 GMT+0000). pub const MAX: Self = Mtp(u32::MAX); + /// Constructs an [`Mtp`] by computing the median‐time‐past from the last 11 block timestamps + /// + /// Because block timestamps are not monotonic, this function internally sorts them; + /// it is therefore not important what order they appear in the array; use whatever + /// is most convenient. + /// + /// # Errors + /// + /// If the median block timestamp is not in the allowable range of MTPs in a + /// locktime: `[500_000_000, 2^32 - 1]`. Because there is a consensus rule that MTP + /// be monotonically increasing, and the MTP of the first 11 blocks exceeds `500_000_000` + /// for every real-life chain, this error typically cannot be hit in practice. + pub fn new(timestamps: [crate::BlockTime; 11]) -> Result { + crate::BlockMtp::new(timestamps).try_into() + } + /// Constructs a new [`Mtp`] from a big-endian hex-encoded `u32`. /// /// The input string may or may not contain a typical hex prefix e.g., `0x`. @@ -539,4 +555,32 @@ mod tests { assert_eq!(format!("{}", blocks), "expected lock-by-blockheight (must be < 500000000)"); assert_eq!(format!("{}", seconds), "expected lock-by-blocktime (must be >= 500000000)"); } + + #[test] + fn valid_chain_computes_mtp() { + use crate::BlockTime; + + let mut timestamps = [ + BlockTime::from_u32(500_000_010), + BlockTime::from_u32(500_000_003), + BlockTime::from_u32(500_000_005), + BlockTime::from_u32(500_000_008), + BlockTime::from_u32(500_000_001), + BlockTime::from_u32(500_000_004), + BlockTime::from_u32(500_000_006), + BlockTime::from_u32(500_000_009), + BlockTime::from_u32(500_000_002), + BlockTime::from_u32(500_000_007), + BlockTime::from_u32(500_000_000), + ]; + + // Try various reorderings + assert_eq!(Mtp::new(timestamps).unwrap().to_u32(), 500_000_005); + timestamps.reverse(); + assert_eq!(Mtp::new(timestamps).unwrap().to_u32(), 500_000_005); + timestamps.sort(); + assert_eq!(Mtp::new(timestamps).unwrap().to_u32(), 500_000_005); + timestamps.reverse(); + assert_eq!(Mtp::new(timestamps).unwrap().to_u32(), 500_000_005); + } } From dcbdb7ca8ac3b26d03cbff5a6532eb208169917e Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Tue, 6 May 2025 15:32:16 +0000 Subject: [PATCH 06/10] units: add checked arithmetic to Block{Height,Mtp}{Interval,} --- units/src/block.rs | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/units/src/block.rs b/units/src/block.rs index c94c9a3ab..dcb483f1c 100644 --- a/units/src/block.rs +++ b/units/src/block.rs @@ -89,6 +89,18 @@ impl_u32_wrapper! { pub struct BlockHeight(pub u32); } +impl BlockHeight { + /// Attempt to subtract two [`BlockHeight`]s, returning `None` in case of overflow. + pub fn checked_sub(self, other: Self) -> Option { + self.0.checked_sub(other.0).map(BlockHeightInterval) + } + + /// Attempt to add an interval to this [`BlockHeight`], returning `None` in case of overflow. + pub fn checked_add(self, other: BlockHeightInterval) -> Option { + self.0.checked_add(other.0).map(Self) + } +} + impl From for BlockHeight { /// Converts a [`locktime::absolute::Height`] to a [`BlockHeight`]. /// @@ -122,6 +134,14 @@ impl_u32_wrapper! { pub struct BlockHeightInterval(pub u32); } +impl BlockHeightInterval { + /// Attempt to subtract two [`BlockHeightInterval`]s, returning `None` in case of overflow. + pub fn checked_sub(self, other: Self) -> Option { self.0.checked_sub(other.0).map(Self) } + + /// Attempt to add two [`BlockHeightInterval`]s, returning `None` in case of overflow. + pub fn checked_add(self, other: Self) -> Option { self.0.checked_add(other.0).map(Self) } +} + impl From for BlockHeightInterval { /// Converts a [`locktime::relative::HeightInterval`] to a [`BlockHeightInterval`]. /// @@ -170,6 +190,16 @@ impl BlockMtp { timestamps.sort_unstable(); Self::from_u32(u32::from(timestamps[5])) } + + /// Attempt to subtract two [`BlockMtp`]s, returning `None` in case of overflow. + pub fn checked_sub(self, other: Self) -> Option { + self.0.checked_sub(other.0).map(BlockMtpInterval) + } + + /// Attempt to add an interval to this [`BlockMtp`], returning `None` in case of overflow. + pub fn checked_add(self, other: BlockMtpInterval) -> Option { + self.0.checked_add(other.0).map(Self) + } } impl From for BlockMtp { @@ -235,6 +265,12 @@ impl BlockMtpInterval { ) -> Result { relative::MtpInterval::from_seconds_ceil(self.to_u32()) } + + /// Attempt to subtract two [`BlockMtpInterval`]s, returning `None` in case of overflow. + pub fn checked_sub(self, other: Self) -> Option { self.0.checked_sub(other.0).map(Self) } + + /// Attempt to add two [`BlockMtpInterval`]s, returning `None` in case of overflow. + pub fn checked_add(self, other: Self) -> Option { self.0.checked_add(other.0).map(Self) } } impl From for BlockMtpInterval { From d933c754f50e24286f7a0da5e3f52288ce75c1a0 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Tue, 6 May 2025 15:39:40 +0000 Subject: [PATCH 07/10] units: change type of MtpHeight::to_mtp to BlockMtp We are going to delete MtpHeight in a couple commits, but to let us do the transition with smaller diffs, we will first change it to be easily convertible to a BlockHeight/BlockMtp pair. --- units/src/mtp_height.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/units/src/mtp_height.rs b/units/src/mtp_height.rs index 74b7705ad..297001083 100644 --- a/units/src/mtp_height.rs +++ b/units/src/mtp_height.rs @@ -2,14 +2,14 @@ //! Median Time Past (MTP) and height - used for working lock times. -use crate::{BlockHeight, BlockTime}; +use crate::{BlockHeight, BlockMtp, BlockTime}; /// A structure containing both Median Time Past (MTP) and current /// absolute block height, used for validating relative locktimes. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct MtpAndHeight { /// The Median Time Past (median of the last 11 blocks' timestamps) - mtp: BlockTime, + mtp: BlockMtp, /// The current block height, height: BlockHeight, } @@ -26,15 +26,11 @@ impl MtpAndHeight { /// - … /// - `timestamps[10]` is the timestamp at height `height` pub fn new(height: BlockHeight, timestamps: [BlockTime; 11]) -> Self { - let mut mtp_timestamps = timestamps; - mtp_timestamps.sort_unstable(); - let mtp = mtp_timestamps[5]; - - MtpAndHeight { mtp, height } + MtpAndHeight { mtp: BlockMtp::new(timestamps), height } } /// Returns the median-time-past component. - pub fn to_mtp(self) -> BlockTime { self.mtp } + pub fn to_mtp(self) -> BlockMtp { self.mtp } /// Returns the block-height component. pub fn to_height(self) -> BlockHeight { self.height } From 72d5fbad737bf02c0bdc843bf4361bfc558c7520 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Mon, 5 May 2025 19:58:58 +0000 Subject: [PATCH 08/10] units: stop using MtpAndHeight in locktime::relative is_satisfied_by methods Keep using it in primitives; will remove it there in the next commit. --- primitives/src/locktime/relative.rs | 6 +- units/src/locktime/relative.rs | 88 ++++++++++++++++------------- 2 files changed, 52 insertions(+), 42 deletions(-) diff --git a/primitives/src/locktime/relative.rs b/primitives/src/locktime/relative.rs index 053e883cf..db0a40552 100644 --- a/primitives/src/locktime/relative.rs +++ b/primitives/src/locktime/relative.rs @@ -255,8 +255,10 @@ impl LockTime { /// ``` pub fn is_satisfied_by(self, chain_tip: MtpAndHeight, utxo_mined_at: MtpAndHeight) -> bool { match self { - LockTime::Blocks(blocks) => blocks.is_satisfied_by(chain_tip, utxo_mined_at), - LockTime::Time(time) => time.is_satisfied_by(chain_tip, utxo_mined_at), + LockTime::Blocks(blocks) => + blocks.is_satisfied_by(chain_tip.to_height(), utxo_mined_at.to_height()), + LockTime::Time(time) => + time.is_satisfied_by(chain_tip.to_mtp(), utxo_mined_at.to_mtp()), } } diff --git a/units/src/locktime/relative.rs b/units/src/locktime/relative.rs index a98b11fb7..af3136d09 100644 --- a/units/src/locktime/relative.rs +++ b/units/src/locktime/relative.rs @@ -9,8 +9,6 @@ use arbitrary::{Arbitrary, Unstructured}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -use crate::mtp_height::MtpAndHeight; - #[deprecated(since = "TBD", note = "use `HeightIterval` instead")] #[doc(hidden)] pub type Height = HeightInterval; @@ -59,21 +57,26 @@ impl HeightInterval { /// Determines whether a relative‐height locktime has matured, taking into account /// both the chain tip and the height at which the UTXO was confirmed. /// + /// If you have two height intervals `x` and `y`, and want to know whether `x` + /// is satisfied by `y`, use `x >= y`. + /// /// # Parameters - /// - `self` – The relative block‐height delay (`h`) required after confirmation. - /// - `chain_tip` – The current chain state (contains the tip height). - /// - `utxo_mined_at` – The chain state at the UTXO’s confirmation block (contains that height). + /// - `self` – the relative block‐height delay (`h`) required after confirmation. + /// - `chain_tip` – the height of the current chain tip + /// - `utxo_mined_at` – the height of the UTXO’s confirmation block /// /// # Returns /// - `true` if a UTXO locked by `self` can be spent in a block after `chain_tip`. /// - `false` if the UTXO is still locked at `chain_tip`. - pub fn is_satisfied_by(self, chain_tip: MtpAndHeight, utxo_mined_at: MtpAndHeight) -> bool { - // let chain_tip_height = BlockHeight::from(chain_tip); - // let utxo_mined_at_height = BlockHeight::from(utxo_mined_at); - match u32::from(self.to_height()).checked_add(utxo_mined_at.to_height().to_u32()) { - Some(target_height) => chain_tip.to_height().to_u32() >= target_height, - None => false, - } + pub fn is_satisfied_by( + self, + chain_tip: crate::BlockHeight, + utxo_mined_at: crate::BlockHeight, + ) -> bool { + chain_tip + .checked_sub(utxo_mined_at) + .and_then(|diff: crate::BlockHeightInterval| diff.try_into().ok()) + .map_or(false, |diff: Self| diff >= self) } } @@ -180,22 +183,26 @@ impl MtpInterval { /// Determines whether a relative‑time lock has matured, taking into account both /// the UTXO’s Median Time Past at confirmation and the required delay. /// + /// If you have two MTP intervals `x` and `y`, and want to know whether `x` + /// is satisfied by `y`, use `x >= y`. + /// /// # Parameters - /// - `self` – The relative time delay (`t`) in 512‑second intervals. - /// - `chain_tip` – The current chain state, providing the tip’s MTP. - /// - `utxo_mined_at` – The chain state at the UTXO’s confirmation, providing its MTP. + /// - `self` – the relative time delay (`t`) in 512‑second intervals. + /// - `chain_tip` – the MTP of the current chain tip + /// - `utxo_mined_at` – the MTP of the UTXO’s confirmation block /// /// # Returns /// - `true` if the relative‐time lock has expired by the tip’s MTP /// - `false` if the lock has not yet expired by the tip’s MTP - pub fn is_satisfied_by(self, chain_tip: MtpAndHeight, utxo_mined_at: MtpAndHeight) -> bool { - match u32::from(self.to_512_second_intervals()).checked_mul(512) { - Some(seconds) => match seconds.checked_add(utxo_mined_at.to_mtp().to_u32()) { - Some(required_seconds) => chain_tip.to_mtp().to_u32() >= required_seconds, - None => false, - }, - None => false, - } + pub fn is_satisfied_by( + self, + chain_tip: crate::BlockMtp, + utxo_mined_at: crate::BlockMtp, + ) -> bool { + chain_tip + .checked_sub(utxo_mined_at) + .and_then(|diff: crate::BlockMtpInterval| diff.to_relative_mtp_interval_floor().ok()) + .map_or(false, |diff: Self| diff >= self) } } @@ -270,7 +277,7 @@ mod tests { use internals::serde_round_trip; use super::*; - use crate::{BlockHeight, BlockTime}; + use crate::BlockTime; const MAXIMUM_ENCODABLE_SECONDS: u32 = u16::MAX as u32 * 512; @@ -355,6 +362,8 @@ mod tests { #[test] fn test_time_chain_state() { + use crate::BlockMtp; + let timestamps: [BlockTime; 11] = generate_timestamps(1_600_000_000, 200); let utxo_timestamps: [BlockTime; 11] = generate_timestamps(1_599_000_000, 200); @@ -367,49 +376,48 @@ mod tests { // Test case 1: Satisfaction (current_mtp >= utxo_mtp + required_seconds) // 10 intervals × 512 seconds = 5120 seconds let time_lock = MtpInterval::from_512_second_intervals(10); - let chain_state1 = MtpAndHeight::new(BlockHeight::from_u32(100), timestamps); - let utxo_state1 = MtpAndHeight::new(BlockHeight::from_u32(80), utxo_timestamps); + let chain_state1 = BlockMtp::new(timestamps); + let utxo_state1 = BlockMtp::new(utxo_timestamps); assert!(time_lock.is_satisfied_by(chain_state1, utxo_state1)); // Test case 2: Not satisfied (current_mtp < utxo_mtp + required_seconds) - let chain_state2 = MtpAndHeight::new(BlockHeight::from_u32(100), timestamps2); - let utxo_state2 = MtpAndHeight::new(BlockHeight::from_u32(80), utxo_timestamps2); + let chain_state2 = BlockMtp::new(timestamps2); + let utxo_state2 = BlockMtp::new(utxo_timestamps2); assert!(!time_lock.is_satisfied_by(chain_state2, utxo_state2)); // Test case 3: Test with a larger value (100 intervals = 51200 seconds) let larger_lock = MtpInterval::from_512_second_intervals(100); - let chain_state3 = MtpAndHeight::new(BlockHeight::from_u32(100), timestamps3); - let utxo_state3 = MtpAndHeight::new(BlockHeight::from_u32(80), utxo_timestamps3); + let chain_state3 = BlockMtp::new(timestamps3); + let utxo_state3 = BlockMtp::new(utxo_timestamps3); assert!(larger_lock.is_satisfied_by(chain_state3, utxo_state3)); // Test case 4: Overflow handling - tests that is_satisfied_by handles overflow gracefully let max_time_lock = MtpInterval::MAX; - let chain_state4 = MtpAndHeight::new(BlockHeight::from_u32(100), timestamps); - let utxo_state4 = MtpAndHeight::new(BlockHeight::from_u32(80), utxo_timestamps); + let chain_state4 = BlockMtp::new(timestamps); + let utxo_state4 = BlockMtp::new(utxo_timestamps); assert!(!max_time_lock.is_satisfied_by(chain_state4, utxo_state4)); } #[test] fn test_height_chain_state() { - let timestamps: [BlockTime; 11] = generate_timestamps(1_600_000_000, 200); - let utxo_timestamps: [BlockTime; 11] = generate_timestamps(1_599_000_000, 200); + use crate::BlockHeight; let height_lock = HeightInterval(10); // Test case 1: Satisfaction (current_height >= utxo_height + required) - let chain_state1 = MtpAndHeight::new(BlockHeight::from_u32(100), timestamps); - let utxo_state1 = MtpAndHeight::new(BlockHeight::from_u32(80), utxo_timestamps); + let chain_state1 = BlockHeight::from_u32(100); + let utxo_state1 = BlockHeight::from_u32(80); assert!(height_lock.is_satisfied_by(chain_state1, utxo_state1)); // Test case 2: Not satisfied (current_height < utxo_height + required) - let chain_state2 = MtpAndHeight::new(BlockHeight::from_u32(89), timestamps); - let utxo_state2 = MtpAndHeight::new(BlockHeight::from_u32(80), utxo_timestamps); + let chain_state2 = BlockHeight::from_u32(89); + let utxo_state2 = BlockHeight::from_u32(80); assert!(!height_lock.is_satisfied_by(chain_state2, utxo_state2)); // Test case 3: Overflow handling - tests that is_satisfied_by handles overflow gracefully let max_height_lock = HeightInterval::MAX; - let chain_state3 = MtpAndHeight::new(BlockHeight::from_u32(1000), timestamps); - let utxo_state3 = MtpAndHeight::new(BlockHeight::from_u32(80), utxo_timestamps); + let chain_state3 = BlockHeight::from_u32(1000); + let utxo_state3 = BlockHeight::from_u32(80); assert!(!max_height_lock.is_satisfied_by(chain_state3, utxo_state3)); } } From d82b8c0bcb46fef945c8d9915c001796b380d5ec Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Tue, 6 May 2025 19:04:56 +0000 Subject: [PATCH 09/10] primitives: stop using MtpAndHeight --- primitives/src/locktime/relative.rs | 89 +++++++++++++++++------------ 1 file changed, 54 insertions(+), 35 deletions(-) diff --git a/primitives/src/locktime/relative.rs b/primitives/src/locktime/relative.rs index db0a40552..c36faf7c5 100644 --- a/primitives/src/locktime/relative.rs +++ b/primitives/src/locktime/relative.rs @@ -14,7 +14,7 @@ use crate::{relative, TxIn}; #[rustfmt::skip] // Keep public re-exports separate. #[doc(inline)] pub use units::locktime::relative::{HeightInterval, MtpInterval, TimeOverflowError}; -use units::mtp_height::MtpAndHeight; +use units::{BlockHeight, BlockMtp}; #[deprecated(since = "TBD", note = "use `Mtp` instead")] #[doc(hidden)] @@ -44,9 +44,7 @@ pub type Time = MtpInterval; /// /// ``` /// use bitcoin_primitives::relative; -/// use bitcoin_primitives::BlockTime; -/// use bitcoin_primitives::BlockHeight; -/// use units::mtp_height::MtpAndHeight; +/// use bitcoin_primitives::{BlockHeight, BlockMtp, BlockTime}; /// let lock_by_height = relative::LockTime::from_height(144); // 144 blocks, approx 24h. /// assert!(lock_by_height.is_block_height()); /// @@ -65,14 +63,15 @@ pub type Time = MtpInterval; /// let utxo_timestamps: [BlockTime; 11] = generate_timestamps(1_599_000_000, 200); /// /// let current_height = BlockHeight::from(100); -/// let utxo_height = BlockHeight::from(80); +/// let current_mtp = BlockMtp::new(timestamps); +/// +/// let utxo_height = BlockHeight::from(80); +/// let utxo_mtp = BlockMtp::new(utxo_timestamps); /// -/// let chain_tip = MtpAndHeight::new(current_height, timestamps); -/// let utxo_mined_at = MtpAndHeight::new(utxo_height, utxo_timestamps); /// let locktime = relative::LockTime::Time(relative::MtpInterval::from_512_second_intervals(10)); /// /// // Check if locktime is satisfied -/// assert!(locktime.is_satisfied_by(chain_tip, utxo_mined_at)); +/// assert!(locktime.is_satisfied_by(current_height, current_mtp, utxo_height, utxo_mtp)); /// ``` #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -227,10 +226,8 @@ impl LockTime { /// ```rust /// # use bitcoin_primitives::locktime::relative::HeightInterval; /// # use bitcoin_primitives::relative::Time; - /// # use units::mtp_height::MtpAndHeight; - /// # use bitcoin_primitives::BlockHeight; + /// # use bitcoin_primitives::{BlockHeight, BlockMtp, BlockTime}; /// # use bitcoin_primitives::relative::LockTime; - /// # use bitcoin_primitives::BlockTime; /// /// fn generate_timestamps(start: u32, step: u16) -> [BlockTime; 11] { /// let mut timestamps = [BlockTime::from_u32(0); 11]; @@ -244,21 +241,26 @@ impl LockTime { /// let utxo_timestamps: [BlockTime; 11] = generate_timestamps(1_599_000_000, 200); /// /// let current_height = BlockHeight::from_u32(100); + /// let current_mtp = BlockMtp::new(timestamps); /// let utxo_height = BlockHeight::from_u32(80); + /// let utxo_mtp = BlockMtp::new(utxo_timestamps); /// - /// let chain_tip = MtpAndHeight::new(current_height, timestamps); - /// let utxo_mined_at = MtpAndHeight::new(utxo_height, utxo_timestamps); /// let locktime = LockTime::Time(Time::from_512_second_intervals(10)); /// /// // Check if locktime is satisfied - /// assert!(locktime.is_satisfied_by(chain_tip, utxo_mined_at)); + /// assert!(locktime.is_satisfied_by(current_height, current_mtp, utxo_height, utxo_mtp)); /// ``` - pub fn is_satisfied_by(self, chain_tip: MtpAndHeight, utxo_mined_at: MtpAndHeight) -> bool { + pub fn is_satisfied_by( + self, + chain_tip_height: BlockHeight, + chain_tip_mtp: BlockMtp, + utxo_mined_at_height: BlockHeight, + utxo_mined_at_mtp: BlockMtp, + ) -> bool { match self { LockTime::Blocks(blocks) => - blocks.is_satisfied_by(chain_tip.to_height(), utxo_mined_at.to_height()), - LockTime::Time(time) => - time.is_satisfied_by(chain_tip.to_mtp(), utxo_mined_at.to_mtp()), + blocks.is_satisfied_by(chain_tip_height, utxo_mined_at_height), + LockTime::Time(time) => time.is_satisfied_by(chain_tip_mtp, utxo_mined_at_mtp), } } @@ -698,38 +700,55 @@ mod tests { let timestamps: [BlockTime; 11] = generate_timestamps(1_600_000_000, 200); let utxo_timestamps: [BlockTime; 11] = generate_timestamps(1_599_000_000, 200); - let chain_tip = MtpAndHeight::new(BlockHeight::from_u32(100), timestamps); - let utxo_mined_at = MtpAndHeight::new(BlockHeight::from_u32(80), utxo_timestamps); + let chain_height = BlockHeight::from_u32(100); + let chain_mtp = BlockMtp::new(timestamps); + let utxo_height = BlockHeight::from_u32(80); + let utxo_mtp = BlockMtp::new(utxo_timestamps); let lock1 = LockTime::Blocks(HeightInterval::from(10)); - assert!(lock1.is_satisfied_by(chain_tip, utxo_mined_at)); + assert!(lock1.is_satisfied_by(chain_height, chain_mtp, utxo_height, utxo_mtp)); let lock2 = LockTime::Blocks(HeightInterval::from(21)); - assert!(!lock2.is_satisfied_by(chain_tip, utxo_mined_at)); + assert!(!lock2.is_satisfied_by(chain_height, chain_mtp, utxo_height, utxo_mtp)); let lock3 = LockTime::Time(MtpInterval::from_512_second_intervals(10)); - assert!(lock3.is_satisfied_by(chain_tip, utxo_mined_at)); + assert!(lock3.is_satisfied_by(chain_height, chain_mtp, utxo_height, utxo_mtp)); let lock4 = LockTime::Time(MtpInterval::from_512_second_intervals(20000)); - assert!(!lock4.is_satisfied_by(chain_tip, utxo_mined_at)); + assert!(!lock4.is_satisfied_by(chain_height, chain_mtp, utxo_height, utxo_mtp)); - assert!(LockTime::ZERO.is_satisfied_by(chain_tip, utxo_mined_at)); - assert!(LockTime::from_512_second_intervals(0).is_satisfied_by(chain_tip, utxo_mined_at)); + assert!(LockTime::ZERO.is_satisfied_by(chain_height, chain_mtp, utxo_height, utxo_mtp)); + assert!(LockTime::from_512_second_intervals(0).is_satisfied_by( + chain_height, + chain_mtp, + utxo_height, + utxo_mtp + )); let lock6 = LockTime::from_seconds_floor(5000).unwrap(); - assert!(lock6.is_satisfied_by(chain_tip, utxo_mined_at)); + assert!(lock6.is_satisfied_by(chain_height, chain_mtp, utxo_height, utxo_mtp)); let max_height_lock = LockTime::Blocks(HeightInterval::MAX); - assert!(!max_height_lock.is_satisfied_by(chain_tip, utxo_mined_at)); + assert!(!max_height_lock.is_satisfied_by(chain_height, chain_mtp, utxo_height, utxo_mtp)); let max_time_lock = LockTime::Time(MtpInterval::MAX); - assert!(!max_time_lock.is_satisfied_by(chain_tip, utxo_mined_at)); + assert!(!max_time_lock.is_satisfied_by(chain_height, chain_mtp, utxo_height, utxo_mtp)); - let max_chain_tip = - MtpAndHeight::new(BlockHeight::from_u32(u32::MAX), generate_timestamps(u32::MAX, 100)); - let max_utxo_mined_at = - MtpAndHeight::new(BlockHeight::MAX, generate_timestamps(u32::MAX, 100)); - assert!(!max_height_lock.is_satisfied_by(max_chain_tip, max_utxo_mined_at)); - assert!(!max_time_lock.is_satisfied_by(max_chain_tip, max_utxo_mined_at)); + let max_chain_height = BlockHeight::from_u32(u32::MAX); + let max_chain_mtp = BlockMtp::new(generate_timestamps(u32::MAX, 100)); + let max_utxo_height = BlockHeight::MAX; + let max_utxo_mtp = max_chain_mtp; + assert!(!max_height_lock.is_satisfied_by( + max_chain_height, + max_chain_mtp, + max_utxo_height, + max_utxo_mtp + )); + assert!(!max_time_lock.is_satisfied_by( + max_chain_height, + max_chain_mtp, + max_utxo_height, + max_utxo_mtp + )); } } From 47c77afaac75706e3404dd576ca70fb8af1ea026 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Tue, 6 May 2025 19:06:24 +0000 Subject: [PATCH 10/10] units: delete MtpAndHeight type This type provides little value beyond just pairing a BlockHeight and a BlockMtp. --- units/src/lib.rs | 1 - units/src/mtp_height.rs | 64 ----------------------------------------- 2 files changed, 65 deletions(-) delete mode 100644 units/src/mtp_height.rs diff --git a/units/src/lib.rs b/units/src/lib.rs index e5f5dda25..4625a7cb6 100644 --- a/units/src/lib.rs +++ b/units/src/lib.rs @@ -48,7 +48,6 @@ pub mod amount; pub mod block; pub mod fee_rate; pub mod locktime; -pub mod mtp_height; pub mod parse; pub mod time; pub mod weight; diff --git a/units/src/mtp_height.rs b/units/src/mtp_height.rs deleted file mode 100644 index 297001083..000000000 --- a/units/src/mtp_height.rs +++ /dev/null @@ -1,64 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 - -//! Median Time Past (MTP) and height - used for working lock times. - -use crate::{BlockHeight, BlockMtp, BlockTime}; - -/// A structure containing both Median Time Past (MTP) and current -/// absolute block height, used for validating relative locktimes. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub struct MtpAndHeight { - /// The Median Time Past (median of the last 11 blocks' timestamps) - mtp: BlockMtp, - /// The current block height, - height: BlockHeight, -} - -impl MtpAndHeight { - /// Constructs an [`MtpAndHeight`] by computing the median‐time‐past from the last 11 block timestamps - /// - /// # Parameters - /// - /// * `height` - The absolute height of the chain tip - /// * `timestamps` - An array of timestamps from the most recent 11 blocks, where - /// - `timestamps[0]` is the timestamp at height `height - 10` - /// - `timestamps[1]` is the timestamp at height `height - 9` - /// - … - /// - `timestamps[10]` is the timestamp at height `height` - pub fn new(height: BlockHeight, timestamps: [BlockTime; 11]) -> Self { - MtpAndHeight { mtp: BlockMtp::new(timestamps), height } - } - - /// Returns the median-time-past component. - pub fn to_mtp(self) -> BlockMtp { self.mtp } - - /// Returns the block-height component. - pub fn to_height(self) -> BlockHeight { self.height } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn valid_chain_computes_mtp() { - let height = BlockHeight::from_u32(100); - let timestamps = [ - BlockTime::from_u32(10), - BlockTime::from_u32(3), - BlockTime::from_u32(5), - BlockTime::from_u32(8), - BlockTime::from_u32(1), - BlockTime::from_u32(4), - BlockTime::from_u32(6), - BlockTime::from_u32(9), - BlockTime::from_u32(2), - BlockTime::from_u32(7), - BlockTime::from_u32(0), - ]; - - let result = MtpAndHeight::new(height, timestamps); - assert_eq!(result.height, height); - assert_eq!(result.mtp.to_u32(), 5); - } -}