From cb882c5ce1fe6fa3e67142b89a9050f6e832be4b Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Mon, 5 May 2025 22:19:29 +0000 Subject: [PATCH] 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) }