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 e5bddd50d..a57ababde 100644 --- a/bitcoin/src/lib.rs +++ b/bitcoin/src/lib.rs @@ -135,12 +135,16 @@ pub use primitives::{ #[doc(inline)] pub use units::{ amount::{Amount, Denomination, SignedAmount}, - block::{BlockHeight, BlockInterval}, + 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}, @@ -243,7 +247,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}; @@ -275,5 +279,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 b3b15958a..2a22b6060 100644 --- a/primitives/src/lib.rs +++ b/primitives/src/lib.rs @@ -52,12 +52,16 @@ pub mod witness; #[doc(inline)] pub use units::{ amount::{self, Amount, SignedAmount}, - block::{BlockHeight, BlockInterval}, + 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/primitives/src/locktime/relative.rs b/primitives/src/locktime/relative.rs index 0aea66f85..b38e2e16c 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))] @@ -226,10 +225,8 @@ impl LockTime { /// /// ```rust /// # 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]; @@ -243,19 +240,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, utxo_mined_at), - LockTime::Time(time) => time.is_satisfied_by(chain_tip, utxo_mined_at), + LockTime::Blocks(blocks) => + 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), } } @@ -695,38 +699,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 + )); } } diff --git a/units/src/block.rs b/units/src/block.rs index 000f0995b..dcb483f1c 100644 --- a/units/src/block.rs +++ b/units/src/block.rs @@ -22,54 +22,90 @@ 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 $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 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) } + + /// 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_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 BlockHeight { - /// Block height 0, the genesis block. - pub const ZERO: Self = BlockHeight(0); + /// 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) + } - /// 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); - - /// 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 } -} - -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() } + /// 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`]. /// /// 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()) } } @@ -79,72 +115,49 @@ 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()) } } -/// 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_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 BlockHeightInterval(pub u32); } -impl fmt::Display for BlockInterval { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) } +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) } } -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 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) { @@ -154,6 +167,121 @@ 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])) + } + + /// 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 { + /// 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()) } +} + +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()) + } + + /// 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 { + /// 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); @@ -175,96 +303,141 @@ 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) + } + } + + // 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!(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) } } -#[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)?)), - } +impl core::iter::Sum for BlockMtpInterval { + fn sum>(iter: I) -> Self { + let sum = iter.map(|interval| interval.0).sum(); + BlockMtpInterval::from_u32(sum) } } -#[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)?)), - } +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) } } @@ -277,17 +450,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()); } @@ -295,39 +470,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 84c435a76..06d24d718 100644 --- a/units/src/lib.rs +++ b/units/src/lib.rs @@ -50,7 +50,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; @@ -59,10 +58,14 @@ pub mod weight; #[rustfmt::skip] pub use self::{ amount::{Amount, SignedAmount}, - block::{BlockHeight, BlockInterval}, + 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/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); + } } diff --git a/units/src/locktime/relative.rs b/units/src/locktime/relative.rs index 47dde70aa..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) } } @@ -154,6 +157,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] @@ -174,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) } } @@ -264,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; @@ -349,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); @@ -361,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)); } } diff --git a/units/src/mtp_height.rs b/units/src/mtp_height.rs deleted file mode 100644 index 74b7705ad..000000000 --- a/units/src/mtp_height.rs +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 - -//! Median Time Past (MTP) and height - used for working lock times. - -use crate::{BlockHeight, 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, - /// 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 { - let mut mtp_timestamps = timestamps; - mtp_timestamps.sort_unstable(); - let mtp = mtp_timestamps[5]; - - MtpAndHeight { mtp, height } - } - - /// Returns the median-time-past component. - pub fn to_mtp(self) -> BlockTime { 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); - } -} diff --git a/units/tests/api.rs b/units/tests/api.rs index 41b39bb4b..a1956d10a 100644 --- a/units/tests/api.rs +++ b/units/tests/api.rs @@ -14,8 +14,8 @@ 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, - BlockTime, FeeRate, SignedAmount, Weight, + amount, block, fee_rate, locktime, parse, weight, Amount, BlockHeight, BlockInterval, BlockMtp, + BlockMtpInterval, BlockTime, FeeRate, SignedAmount, Weight, }; /// A struct that includes all public non-error enums. @@ -43,6 +43,8 @@ struct Structs { j: relative::Time, k: Weight, l: BlockTime, + m: BlockMtp, + n: BlockMtpInterval, } impl Structs { @@ -60,6 +62,8 @@ impl Structs { j: relative::Time::MAX, k: Weight::MAX, l: BlockTime::from_u32(u32::MAX), + m: BlockMtp::MAX, + n: BlockMtpInterval::MAX, } } } @@ -90,6 +94,8 @@ struct CommonTraits { j: relative::Time, k: Weight, l: BlockTime, + m: BlockMtp, + n: BlockMtpInterval, } /// A struct that includes all types that implement `Default`. @@ -100,6 +106,7 @@ struct Default { c: BlockInterval, d: relative::Height, e: relative::Time, + f: BlockMtpInterval, } /// A struct that includes all public error types. @@ -147,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, BlockTime, FeeRate, SignedAmount, Weight, + Amount, BlockHeight, BlockInterval, BlockMtp, BlockMtpInterval, BlockTime, FeeRate, + SignedAmount, Weight, }; } @@ -164,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, }; } @@ -257,6 +265,7 @@ fn regression_default() { c: BlockInterval::ZERO, d: relative::Height::ZERO, e: relative::Time::ZERO, + f: BlockMtpInterval::ZERO, }; assert_eq!(got, want); } @@ -298,6 +307,8 @@ impl<'a> Arbitrary<'a> for Structs { j: relative::Time::arbitrary(u)?, k: Weight::arbitrary(u)?, l: BlockTime::arbitrary(u)?, + m: BlockMtp::arbitrary(u)?, + n: BlockMtpInterval::arbitrary(u)?, }; Ok(a) }