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.
This commit is contained in:
Andrew Poelstra 2025-05-05 22:19:29 +00:00
parent 4e3af5162f
commit cb882c5ce1
No known key found for this signature in database
GPG Key ID: C588D63CE41B97C1
4 changed files with 141 additions and 3 deletions

View File

@ -190,6 +190,62 @@ impl TryFrom<BlockMtp> for absolute::Mtp {
fn try_from(h: BlockMtp) -> Result<Self, Self::Error> { 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, relative::TimeOverflowError> {
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, relative::TimeOverflowError> {
relative::MtpInterval::from_seconds_ceil(self.to_u32())
}
}
impl From<relative::MtpInterval> 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<BlockMtp> 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<BlockMtpInterval> 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<BlockMtpInterval> 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<BlockMtpInterval> 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<BlockMtpInterval> 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<I: Iterator<Item = Self>>(iter: I) -> Self {
@ -280,6 +388,23 @@ impl<'a> core::iter::Sum<&'a BlockInterval> for BlockInterval {
}
}
impl core::iter::Sum for BlockMtpInterval {
fn sum<I: Iterator<Item = Self>>(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<I>(iter: I) -> Self
where
I: Iterator<Item = &'a BlockMtpInterval>,
{
let sum = iter.map(|interval| interval.0).sum();
BlockMtpInterval::from_u32(sum)
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -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,

View File

@ -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]

View File

@ -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)
}