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.
This commit is contained in:
Andrew Poelstra 2025-05-05 22:08:32 +00:00
parent a3228d4636
commit 4e3af5162f
No known key found for this signature in database
GPG Key ID: C588D63CE41B97C1
5 changed files with 54 additions and 9 deletions

View File

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

View File

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

View File

@ -93,8 +93,7 @@ impl From<absolute::Height> 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<BlockHeight> 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<Self, Self::Error> {
absolute::Height::from_u32(h.to_u32())
}
@ -149,6 +147,49 @@ impl TryFrom<BlockInterval> 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 mediantimepast 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<absolute::Mtp> 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<BlockMtp> 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<Self, Self::Error> { 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);

View File

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

View File

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