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