Merge rust-bitcoin/rust-bitcoin#4468: Improve lock times - fix off-by-one bug
4ccecf5dec
Fix stale Height type link (Tobin C. Harding)caebb1bf73
units: relative: Do minor rustdocs fixes (Tobin C. Harding)40bb177bc2
Put is_satisfied_by functions together (Tobin C. Harding)480a2cd62a
Favour new function `from_mtp` over deprecated (Tobin C. Harding)f9d6453d5b
Shorten locktime type term (Tobin C. Harding)727047bd39
Fix off-by-one-bug in absolute locktime (Tobin C. Harding)3ffdc54ca5
Fix off-by-one bug in relative locktime (Tobin C. Harding)a2ff8ddbbb
Improve relative::LockTime is_satisfied_by_{height, time} (Tobin C. Harding) Pull request description: Make the APIs uniform in relative and absolute locktimes in relation to the `is_satisfied_by` functions. In doing so improve the API and fix an off-by-one bug when checking satisfaction of locks by height. Done in three patches but maybe should be squashed? Probably easiest to review by looking at all the `is_satisfied_by*` functions and convincing yourself we got it right. EDIT: Now has 5 cleanup patches also (mostly docs cleanups). ACKs for top commit: apoelstra: ACK 4ccecf5decfead9818b74fbdee73115c349e2f3e; successfully ran local tests Tree-SHA512: 9206cb464a06647510a35a7d564062823117e75df60251969be458616f4f5d04acf0aada53dbf7d493a2a2a72d26b3a300417a6499e45413d5f2a011538b7826
This commit is contained in:
commit
6d8299e8b8
|
@ -32,7 +32,7 @@ pub mod locktime {
|
|||
pub mod absolute {
|
||||
//! Provides type [`LockTime`] that implements the logic around nLockTime/OP_CHECKLOCKTIMEVERIFY.
|
||||
//!
|
||||
//! There are two types of lock time: lock-by-blockheight and lock-by-blocktime, distinguished by
|
||||
//! There are two types of lock time: lock-by-height and lock-by-time, distinguished by
|
||||
//! whether `LockTime < LOCKTIME_THRESHOLD`.
|
||||
|
||||
use io::{BufRead, Write};
|
||||
|
@ -66,12 +66,12 @@ pub mod locktime {
|
|||
pub mod relative {
|
||||
//! Provides type [`LockTime`] that implements the logic around nSequence/OP_CHECKSEQUENCEVERIFY.
|
||||
//!
|
||||
//! There are two types of lock time: lock-by-blockheight and lock-by-blocktime, distinguished by
|
||||
//! There are two types of lock time: lock-by-height and lock-by-time, distinguished by
|
||||
//! whether bit 22 of the `u32` consensus value is set.
|
||||
|
||||
/// Re-export everything from the `primitives::locktime::relative` module.
|
||||
pub use primitives::locktime::relative::{
|
||||
DisabledLockTimeError, IncompatibleHeightError, IncompatibleTimeError, LockTime,
|
||||
DisabledLockTimeError, InvalidHeightError, InvalidTimeError, LockTime,
|
||||
NumberOf512Seconds, NumberOfBlocks, TimeOverflowError,
|
||||
};
|
||||
|
||||
|
|
|
@ -63,7 +63,7 @@ fn serde_regression_absolute_lock_time_height() {
|
|||
#[test]
|
||||
fn serde_regression_absolute_lock_time_time() {
|
||||
let seconds: u32 = 1653195600; // May 22nd, 5am UTC.
|
||||
let t = absolute::LockTime::from_time(seconds).expect("valid time");
|
||||
let t = absolute::LockTime::from_mtp(seconds).expect("valid time");
|
||||
let got = serialize(&t).unwrap();
|
||||
|
||||
let want = include_bytes!("data/serde/absolute_lock_time_seconds_bincode") as &[_];
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
//! Provides type [`LockTime`] that implements the logic around `nLockTime`/`OP_CHECKLOCKTIMEVERIFY`.
|
||||
//!
|
||||
//! There are two types of lock time: lock-by-blockheight and lock-by-blocktime, distinguished by
|
||||
//! There are two types of lock time: lock-by-height and lock-by-time, distinguished by
|
||||
//! whether `LockTime < LOCKTIME_THRESHOLD`.
|
||||
|
||||
use core::fmt;
|
||||
|
@ -79,7 +79,7 @@ pub enum LockTime {
|
|||
/// use bitcoin_primitives::absolute;
|
||||
///
|
||||
/// let seconds: u32 = 1653195600; // May 22nd, 5am UTC.
|
||||
/// let n = absolute::LockTime::from_time(seconds).expect("valid time");
|
||||
/// let n = absolute::LockTime::from_mtp(seconds).expect("valid time");
|
||||
/// assert!(n.is_block_time());
|
||||
/// assert_eq!(n.to_consensus_u32(), seconds);
|
||||
/// ```
|
||||
|
@ -197,8 +197,8 @@ impl LockTime {
|
|||
///
|
||||
/// ```rust
|
||||
/// # use bitcoin_primitives::absolute;
|
||||
/// assert!(absolute::LockTime::from_time(1653195600).is_ok());
|
||||
/// assert!(absolute::LockTime::from_time(741521).is_err());
|
||||
/// assert!(absolute::LockTime::from_mtp(1653195600).is_ok());
|
||||
/// assert!(absolute::LockTime::from_mtp(741521).is_err());
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn from_mtp(n: u32) -> Result<Self, ConversionError> {
|
||||
|
@ -233,13 +233,21 @@ impl LockTime {
|
|||
/// is satisfied if a transaction with `nLockTime` ([`Transaction::lock_time`]) set to
|
||||
/// `height`/`time` is valid.
|
||||
///
|
||||
/// If `height` and `mtp` represent the current chain tip then a transaction with this
|
||||
/// locktime can be broadcast for inclusion in the next block.
|
||||
///
|
||||
/// If you do not have, or do not wish to calculate, both parameters consider using:
|
||||
///
|
||||
/// * [`is_satisfied_by_height()`](absolute::LockTime::is_satisfied_by_height)
|
||||
/// * [`is_satisfied_by_time()`](absolute::LockTime::is_satisfied_by_time)
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use bitcoin_primitives::absolute;
|
||||
/// // Can be implemented if block chain data is available.
|
||||
/// fn get_height() -> absolute::Height { todo!("return the current block height") }
|
||||
/// fn get_time() -> absolute::MedianTimePast { todo!("return the current block time") }
|
||||
/// fn get_time() -> absolute::MedianTimePast { todo!("return the current block MTP") }
|
||||
///
|
||||
/// let n = absolute::LockTime::from_consensus(741521); // `n OP_CHEKCLOCKTIMEVERIFY`.
|
||||
/// if n.is_satisfied_by(get_height(), get_time()) {
|
||||
|
@ -247,12 +255,43 @@ impl LockTime {
|
|||
/// }
|
||||
/// ````
|
||||
#[inline]
|
||||
pub fn is_satisfied_by(self, height: Height, time: MedianTimePast) -> bool {
|
||||
pub fn is_satisfied_by(self, height: Height, mtp: MedianTimePast) -> bool {
|
||||
match self {
|
||||
LockTime::Blocks(blocks) => blocks.is_satisfied_by(height),
|
||||
LockTime::Seconds(time) => time.is_satisfied_by(mtp),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if a transaction with this locktime can be spent in the next block.
|
||||
///
|
||||
/// If `height` is the current block height of the chain then a transaction with this locktime
|
||||
/// can be broadcast for inclusion in the next block.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if this lock is not lock-by-height.
|
||||
#[inline]
|
||||
pub fn is_satisfied_by_height(self, height: Height) -> Result<bool, IncompatibleHeightError> {
|
||||
use LockTime as L;
|
||||
|
||||
match self {
|
||||
L::Blocks(n) => n <= height,
|
||||
L::Seconds(n) => n <= time,
|
||||
L::Blocks(blocks) => Ok(blocks.is_satisfied_by(height)),
|
||||
L::Seconds(time) => Err(IncompatibleHeightError { lock: time, incompatible: height }),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if a transaction with this locktime can be included in the next block.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if this lock is not lock-by-time.
|
||||
#[inline]
|
||||
pub fn is_satisfied_by_time(self, mtp: MedianTimePast) -> Result<bool, IncompatibleTimeError> {
|
||||
use LockTime as L;
|
||||
|
||||
match self {
|
||||
L::Seconds(time) => Ok(time.is_satisfied_by(mtp)),
|
||||
L::Blocks(blocks) => Err(IncompatibleTimeError { lock: blocks, incompatible: mtp }),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -407,6 +446,68 @@ impl<'de> serde::Deserialize<'de> for LockTime {
|
|||
}
|
||||
}
|
||||
|
||||
/// Tried to satisfy a lock-by-time lock using a height value.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct IncompatibleHeightError {
|
||||
/// The inner value of the lock-by-time lock.
|
||||
lock: MedianTimePast,
|
||||
/// Attempted to satisfy a lock-by-time lock with this height.
|
||||
incompatible: Height,
|
||||
}
|
||||
|
||||
impl IncompatibleHeightError {
|
||||
/// Returns the value of the lock-by-time lock.
|
||||
pub fn lock(&self) -> MedianTimePast { self.lock }
|
||||
|
||||
/// Returns the height that was erroneously used to try and satisfy a lock-by-time lock.
|
||||
pub fn incompatible(&self) -> Height { self.incompatible }
|
||||
}
|
||||
|
||||
impl fmt::Display for IncompatibleHeightError {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"tried to satisfy a lock-by-time lock {} with height: {}",
|
||||
self.lock, self.incompatible
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for IncompatibleHeightError {}
|
||||
|
||||
/// Tried to satisfy a lock-by-height lock using a height value.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct IncompatibleTimeError {
|
||||
/// The inner value of the lock-by-height lock.
|
||||
lock: Height,
|
||||
/// Attempted to satisfy a lock-by-height lock with this MTP.
|
||||
incompatible: MedianTimePast,
|
||||
}
|
||||
|
||||
impl IncompatibleTimeError {
|
||||
/// Returns the value of the lock-by-height lock.
|
||||
pub fn lock(&self) -> Height { self.lock }
|
||||
|
||||
/// Returns the MTP that was erroneously used to try and satisfy a lock-by-height lock.
|
||||
pub fn incompatible(&self) -> MedianTimePast { self.incompatible }
|
||||
}
|
||||
|
||||
impl fmt::Display for IncompatibleTimeError {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"tried to satisfy a lock-by-height lock {} with MTP: {}",
|
||||
self.lock, self.incompatible
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for IncompatibleTimeError {}
|
||||
|
||||
#[cfg(feature = "arbitrary")]
|
||||
impl<'a> Arbitrary<'a> for LockTime {
|
||||
fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
|
||||
|
|
|
@ -2,18 +2,20 @@
|
|||
|
||||
//! Provides type [`LockTime`] that implements the logic around `nSequence`/`OP_CHECKSEQUENCEVERIFY`.
|
||||
//!
|
||||
//! There are two types of lock time: lock-by-blockheight and lock-by-blocktime, distinguished by
|
||||
//! There are two types of lock time: lock-by-height and lock-by-time, distinguished by
|
||||
//! whether bit 22 of the `u32` consensus value is set.
|
||||
|
||||
use core::{convert, fmt};
|
||||
|
||||
use internals::write_err;
|
||||
|
||||
use crate::Sequence;
|
||||
#[cfg(all(doc, feature = "alloc"))]
|
||||
use crate::{relative, TxIn};
|
||||
|
||||
#[rustfmt::skip] // Keep public re-exports separate.
|
||||
#[doc(inline)]
|
||||
pub use units::locktime::relative::{NumberOfBlocks, NumberOf512Seconds, TimeOverflowError};
|
||||
pub use units::locktime::relative::{NumberOfBlocks, NumberOf512Seconds, TimeOverflowError, InvalidHeightError, InvalidTimeError};
|
||||
use units::{BlockHeight, BlockMtp};
|
||||
|
||||
#[deprecated(since = "TBD", note = "use `NumberOfBlocks` instead")]
|
||||
|
@ -39,40 +41,6 @@ pub type Time = NumberOf512Seconds;
|
|||
///
|
||||
/// * [BIP 68 Relative lock-time using consensus-enforced sequence numbers](https://github.com/bitcoin/bips/blob/master/bip-0065.mediawiki)
|
||||
/// * [BIP 112 CHECKSEQUENCEVERIFY](https://github.com/bitcoin/bips/blob/master/bip-0112.mediawiki)
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use bitcoin_primitives::relative;
|
||||
/// 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());
|
||||
///
|
||||
/// let lock_by_time = relative::LockTime::from_512_second_intervals(168); // 168 time intervals, approx 24h.
|
||||
/// assert!(lock_by_time.is_block_time());
|
||||
///
|
||||
/// fn generate_timestamps(start: u32, step: u16) -> [BlockTime; 11] {
|
||||
/// let mut timestamps = [BlockTime::from_u32(0); 11];
|
||||
/// for (i, ts) in timestamps.iter_mut().enumerate() {
|
||||
/// *ts = BlockTime::from_u32(start.saturating_sub((step * i as u16).into()));
|
||||
/// }
|
||||
/// timestamps
|
||||
/// }
|
||||
/// // time extracted from BlockHeader
|
||||
/// let timestamps: [BlockTime; 11] = generate_timestamps(1_600_000_000, 200);
|
||||
/// let utxo_timestamps: [BlockTime; 11] = generate_timestamps(1_599_000_000, 200);
|
||||
///
|
||||
/// let current_height = BlockHeight::from(100);
|
||||
/// let current_mtp = BlockMtp::new(timestamps);
|
||||
///
|
||||
/// let utxo_height = BlockHeight::from(80);
|
||||
/// let utxo_mtp = BlockMtp::new(utxo_timestamps);
|
||||
///
|
||||
/// let locktime = relative::LockTime::Time(relative::NumberOf512Seconds::from_512_second_intervals(10));
|
||||
///
|
||||
/// // Check if locktime is satisfied
|
||||
/// 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))]
|
||||
pub enum LockTime {
|
||||
|
@ -221,45 +189,75 @@ impl LockTime {
|
|||
pub const fn is_block_time(self) -> bool { !self.is_block_height() }
|
||||
|
||||
/// Returns true if this [`relative::LockTime`] is satisfied by the given chain state.
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use bitcoin_primitives::relative::Time;
|
||||
/// # use bitcoin_primitives::{BlockHeight, BlockMtp, BlockTime};
|
||||
/// # use bitcoin_primitives::relative::LockTime;
|
||||
/// If this function returns true then an output with this locktime can be spent in the next
|
||||
/// block.
|
||||
///
|
||||
/// fn generate_timestamps(start: u32, step: u16) -> [BlockTime; 11] {
|
||||
/// let mut timestamps = [BlockTime::from_u32(0); 11];
|
||||
/// for (i, ts) in timestamps.iter_mut().enumerate() {
|
||||
/// *ts = BlockTime::from_u32(start.saturating_sub((step * i as u16).into()));
|
||||
/// }
|
||||
/// timestamps
|
||||
/// }
|
||||
/// // time extracted from BlockHeader
|
||||
/// let timestamps: [BlockTime; 11] = generate_timestamps(1_600_000_000, 200);
|
||||
/// let utxo_timestamps: [BlockTime; 11] = generate_timestamps(1_599_000_000, 200);
|
||||
/// # Errors
|
||||
///
|
||||
/// 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 locktime = LockTime::Time(Time::from_512_second_intervals(10));
|
||||
///
|
||||
/// // Check if locktime is satisfied
|
||||
/// assert!(locktime.is_satisfied_by(current_height, current_mtp, utxo_height, utxo_mtp));
|
||||
/// ```
|
||||
/// If `chain_tip` as not _after_ `utxo_mined_at` i.e., if you get the args mixed up.
|
||||
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 {
|
||||
) -> Result<bool, IsSatisfiedByError> {
|
||||
match self {
|
||||
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),
|
||||
LockTime::Blocks(blocks) => blocks
|
||||
.is_satisfied_by(chain_tip_height, utxo_mined_at_height)
|
||||
.map_err(IsSatisfiedByError::Blocks),
|
||||
LockTime::Time(time) => time
|
||||
.is_satisfied_by(chain_tip_mtp, utxo_mined_at_mtp)
|
||||
.map_err(IsSatisfiedByError::Time),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if an output with this locktime can be spent in the next block.
|
||||
///
|
||||
/// If this function returns true then an output with this locktime can be spent in the next
|
||||
/// block.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if this lock is not lock-by-height.
|
||||
#[inline]
|
||||
pub fn is_satisfied_by_height(
|
||||
self,
|
||||
chain_tip: BlockHeight,
|
||||
utxo_mined_at: BlockHeight,
|
||||
) -> Result<bool, IsSatisfiedByHeightError> {
|
||||
use LockTime as L;
|
||||
|
||||
match self {
|
||||
L::Blocks(blocks) => blocks
|
||||
.is_satisfied_by(chain_tip, utxo_mined_at)
|
||||
.map_err(IsSatisfiedByHeightError::Satisfaction),
|
||||
L::Time(time) => Err(IsSatisfiedByHeightError::Incompatible(time)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if an output with this locktime can be spent in the next block.
|
||||
///
|
||||
/// If this function returns true then an output with this locktime can be spent in the next
|
||||
/// block.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if this lock is not lock-by-time.
|
||||
#[inline]
|
||||
pub fn is_satisfied_by_time(
|
||||
self,
|
||||
chain_tip: BlockMtp,
|
||||
utxo_mined_at: BlockMtp,
|
||||
) -> Result<bool, IsSatisfiedByTimeError> {
|
||||
use LockTime as L;
|
||||
|
||||
match self {
|
||||
L::Time(time) => time
|
||||
.is_satisfied_by(chain_tip, utxo_mined_at)
|
||||
.map_err(IsSatisfiedByTimeError::Satisfaction),
|
||||
L::Blocks(blocks) => Err(IsSatisfiedByTimeError::Incompatible(blocks)),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -331,64 +329,6 @@ impl LockTime {
|
|||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if this [`relative::LockTime`] is satisfied by [`NumberOfBlocks`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if this lock is not lock-by-height.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use bitcoin_primitives::Sequence;
|
||||
/// # use bitcoin_primitives::relative;
|
||||
///
|
||||
/// let required_height: u16 = 100;
|
||||
/// let lock = Sequence::from_height(required_height).to_relative_lock_time().expect("valid height");
|
||||
/// assert!(lock.is_satisfied_by_height(relative::NumberOfBlocks::from(required_height + 1)).expect("a height"));
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn is_satisfied_by_height(
|
||||
self,
|
||||
height: NumberOfBlocks,
|
||||
) -> Result<bool, IncompatibleHeightError> {
|
||||
use LockTime as L;
|
||||
|
||||
match self {
|
||||
L::Blocks(required_height) => Ok(required_height <= height),
|
||||
L::Time(time) => Err(IncompatibleHeightError { height, time }),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if this [`relative::LockTime`] is satisfied by [`Time`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if this lock is not lock-by-time.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use bitcoin_primitives::Sequence;
|
||||
/// # use bitcoin_primitives::relative;
|
||||
///
|
||||
/// let intervals: u16 = 70; // approx 10 hours;
|
||||
/// let lock = Sequence::from_512_second_intervals(intervals).to_relative_lock_time().expect("valid time");
|
||||
/// assert!(lock.is_satisfied_by_time(relative::Time::from_512_second_intervals(intervals + 10)).expect("a time"));
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn is_satisfied_by_time(
|
||||
self,
|
||||
time: NumberOf512Seconds,
|
||||
) -> Result<bool, IncompatibleTimeError> {
|
||||
use LockTime as L;
|
||||
|
||||
match self {
|
||||
L::Time(ref t) => Ok(t.to_512_second_intervals() <= time.to_512_second_intervals()),
|
||||
L::Blocks(height) => Err(IncompatibleTimeError { time, height }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NumberOfBlocks> for LockTime {
|
||||
|
@ -454,67 +394,108 @@ impl fmt::Display for DisabledLockTimeError {
|
|||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for DisabledLockTimeError {}
|
||||
|
||||
/// Tried to satisfy a lock-by-blocktime lock using a height value.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct IncompatibleHeightError {
|
||||
/// Attempted to satisfy a lock-by-blocktime lock with this height.
|
||||
height: NumberOfBlocks,
|
||||
/// The inner time value of the lock-by-blocktime lock.
|
||||
time: NumberOf512Seconds,
|
||||
/// Error returned when attempting to satisfy lock fails.
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum IsSatisfiedByError {
|
||||
/// Error when attempting to satisfy lock by height.
|
||||
Blocks(InvalidHeightError),
|
||||
/// Error when attempting to satisfy lock by time.
|
||||
Time(InvalidTimeError),
|
||||
}
|
||||
|
||||
impl IncompatibleHeightError {
|
||||
/// Returns the height that was erroneously used to try and satisfy a lock-by-blocktime lock.
|
||||
pub fn incompatible(&self) -> NumberOfBlocks { self.height }
|
||||
|
||||
/// Returns the time value of the lock-by-blocktime lock.
|
||||
pub fn expected(&self) -> NumberOf512Seconds { self.time }
|
||||
}
|
||||
|
||||
impl fmt::Display for IncompatibleHeightError {
|
||||
impl fmt::Display for IsSatisfiedByError {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"tried to satisfy a lock-by-blocktime lock {} with height: {}",
|
||||
self.time, self.height
|
||||
)
|
||||
use IsSatisfiedByError as E;
|
||||
|
||||
match *self {
|
||||
E::Blocks(ref e) => write_err!(f, "blocks"; e),
|
||||
E::Time(ref e) => write_err!(f, "time"; e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for IncompatibleHeightError {}
|
||||
impl std::error::Error for IsSatisfiedByError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
use IsSatisfiedByError as E;
|
||||
|
||||
/// Tried to satisfy a lock-by-blockheight lock using a time value.
|
||||
match *self {
|
||||
E::Blocks(ref e) => Some(e),
|
||||
E::Time(ref e) => Some(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error returned when `is_satisfied_by_height` fails.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct IncompatibleTimeError {
|
||||
/// Attempted to satisfy a lock-by-blockheight lock with this time.
|
||||
time: NumberOf512Seconds,
|
||||
/// The inner height value of the lock-by-blockheight lock.
|
||||
height: NumberOfBlocks,
|
||||
pub enum IsSatisfiedByHeightError {
|
||||
/// Satisfaction of the lock height value failed.
|
||||
Satisfaction(InvalidHeightError),
|
||||
/// Tried to satisfy a lock-by-height locktime using seconds.
|
||||
// TODO: Hide inner value in a new struct error type.
|
||||
Incompatible(NumberOf512Seconds),
|
||||
}
|
||||
|
||||
impl IncompatibleTimeError {
|
||||
/// Returns the time that was erroneously used to try and satisfy a lock-by-blockheight lock.
|
||||
pub fn incompatible(&self) -> NumberOf512Seconds { self.time }
|
||||
|
||||
/// Returns the height value of the lock-by-blockheight lock.
|
||||
pub fn expected(&self) -> NumberOfBlocks { self.height }
|
||||
}
|
||||
|
||||
impl fmt::Display for IncompatibleTimeError {
|
||||
impl fmt::Display for IsSatisfiedByHeightError {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"tried to satisfy a lock-by-blockheight lock {} with time: {}",
|
||||
self.height, self.time
|
||||
)
|
||||
use IsSatisfiedByHeightError as E;
|
||||
|
||||
match *self {
|
||||
E::Satisfaction(ref e) => write_err!(f, "satisfaction"; e),
|
||||
E::Incompatible(time) =>
|
||||
write!(f, "tried to satisfy a lock-by-height locktime using seconds {}", time),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for IncompatibleTimeError {}
|
||||
impl std::error::Error for IsSatisfiedByHeightError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
use IsSatisfiedByHeightError as E;
|
||||
|
||||
match *self {
|
||||
E::Satisfaction(ref e) => Some(e),
|
||||
E::Incompatible(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error returned when `is_satisfied_by_time` fails.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum IsSatisfiedByTimeError {
|
||||
/// Satisfaction of the lock time value failed.
|
||||
Satisfaction(InvalidTimeError),
|
||||
/// Tried to satisfy a lock-by-time locktime using number of blocks.
|
||||
// TODO: Hide inner value in a new struct error type.
|
||||
Incompatible(NumberOfBlocks),
|
||||
}
|
||||
|
||||
impl fmt::Display for IsSatisfiedByTimeError {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
use IsSatisfiedByTimeError as E;
|
||||
|
||||
match *self {
|
||||
E::Satisfaction(ref e) => write_err!(f, "satisfaction"; e),
|
||||
E::Incompatible(blocks) =>
|
||||
write!(f, "tried to satisfy a lock-by-height locktime using blocks {}", blocks),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for IsSatisfiedByTimeError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
use IsSatisfiedByTimeError as E;
|
||||
|
||||
match *self {
|
||||
E::Satisfaction(ref e) => Some(e),
|
||||
E::Incompatible(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
@ -664,25 +645,29 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn incompatible_height_error() {
|
||||
let height = NumberOfBlocks::from(10);
|
||||
let time = NumberOf512Seconds::from_512_second_intervals(70);
|
||||
let lock_by_time = LockTime::from(time);
|
||||
let err = lock_by_time.is_satisfied_by_height(height).unwrap_err();
|
||||
// This is an error test these values are not used in the error path.
|
||||
let mined_at = BlockHeight::from_u32(700_000);
|
||||
let chain_tip = BlockHeight::from_u32(800_000);
|
||||
|
||||
assert_eq!(err.incompatible(), height);
|
||||
assert_eq!(err.expected(), time);
|
||||
let lock_by_time = LockTime::from_512_second_intervals(70); // Arbitrary value.
|
||||
let err = lock_by_time.is_satisfied_by_height(chain_tip, mined_at).unwrap_err();
|
||||
|
||||
let expected_time = NumberOf512Seconds::from_512_second_intervals(70);
|
||||
assert_eq!(err, IsSatisfiedByHeightError::Incompatible(expected_time));
|
||||
assert!(!format!("{}", err).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incompatible_time_error() {
|
||||
let height = NumberOfBlocks::from(10);
|
||||
let time = NumberOf512Seconds::from_512_second_intervals(70);
|
||||
let lock_by_height = LockTime::from(height);
|
||||
let err = lock_by_height.is_satisfied_by_time(time).unwrap_err();
|
||||
// This is an error test these values are not used in the error path.
|
||||
let mined_at = BlockMtp::from_u32(1_234_567_890);
|
||||
let chain_tip = BlockMtp::from_u32(1_600_000_000);
|
||||
|
||||
assert_eq!(err.incompatible(), time);
|
||||
assert_eq!(err.expected(), height);
|
||||
let lock_by_height = LockTime::from_height(10); // Arbitrary value.
|
||||
let err = lock_by_height.is_satisfied_by_time(chain_tip, mined_at).unwrap_err();
|
||||
|
||||
let expected_height = NumberOfBlocks::from(10);
|
||||
assert_eq!(err, IsSatisfiedByTimeError::Incompatible(expected_height));
|
||||
assert!(!format!("{}", err).is_empty());
|
||||
}
|
||||
|
||||
|
@ -705,49 +690,46 @@ mod tests {
|
|||
let utxo_mtp = BlockMtp::new(utxo_timestamps);
|
||||
|
||||
let lock1 = LockTime::Blocks(NumberOfBlocks::from(10));
|
||||
assert!(lock1.is_satisfied_by(chain_height, chain_mtp, utxo_height, utxo_mtp));
|
||||
assert!(lock1.is_satisfied_by(chain_height, chain_mtp, utxo_height, utxo_mtp).unwrap());
|
||||
|
||||
let lock2 = LockTime::Blocks(NumberOfBlocks::from(21));
|
||||
assert!(!lock2.is_satisfied_by(chain_height, chain_mtp, utxo_height, utxo_mtp));
|
||||
assert!(lock2.is_satisfied_by(chain_height, chain_mtp, utxo_height, utxo_mtp).unwrap());
|
||||
|
||||
let lock3 = LockTime::Time(NumberOf512Seconds::from_512_second_intervals(10));
|
||||
assert!(lock3.is_satisfied_by(chain_height, chain_mtp, utxo_height, utxo_mtp));
|
||||
assert!(lock3.is_satisfied_by(chain_height, chain_mtp, utxo_height, utxo_mtp).unwrap());
|
||||
|
||||
let lock4 = LockTime::Time(NumberOf512Seconds::from_512_second_intervals(20000));
|
||||
assert!(!lock4.is_satisfied_by(chain_height, chain_mtp, utxo_height, utxo_mtp));
|
||||
assert!(!lock4.is_satisfied_by(chain_height, chain_mtp, utxo_height, utxo_mtp).unwrap());
|
||||
|
||||
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
|
||||
));
|
||||
assert!(LockTime::ZERO
|
||||
.is_satisfied_by(chain_height, chain_mtp, utxo_height, utxo_mtp)
|
||||
.unwrap());
|
||||
assert!(LockTime::from_512_second_intervals(0)
|
||||
.is_satisfied_by(chain_height, chain_mtp, utxo_height, utxo_mtp)
|
||||
.unwrap());
|
||||
|
||||
let lock6 = LockTime::from_seconds_floor(5000).unwrap();
|
||||
assert!(lock6.is_satisfied_by(chain_height, chain_mtp, utxo_height, utxo_mtp));
|
||||
assert!(lock6.is_satisfied_by(chain_height, chain_mtp, utxo_height, utxo_mtp).unwrap());
|
||||
|
||||
let max_height_lock = LockTime::Blocks(NumberOfBlocks::MAX);
|
||||
assert!(!max_height_lock.is_satisfied_by(chain_height, chain_mtp, utxo_height, utxo_mtp));
|
||||
assert!(!max_height_lock
|
||||
.is_satisfied_by(chain_height, chain_mtp, utxo_height, utxo_mtp)
|
||||
.unwrap());
|
||||
|
||||
let max_time_lock = LockTime::Time(NumberOf512Seconds::MAX);
|
||||
assert!(!max_time_lock.is_satisfied_by(chain_height, chain_mtp, utxo_height, utxo_mtp));
|
||||
assert!(!max_time_lock
|
||||
.is_satisfied_by(chain_height, chain_mtp, utxo_height, utxo_mtp)
|
||||
.unwrap());
|
||||
|
||||
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
|
||||
));
|
||||
assert!(!max_height_lock
|
||||
.is_satisfied_by(max_chain_height, max_chain_mtp, max_utxo_height, max_utxo_mtp)
|
||||
.unwrap());
|
||||
assert!(!max_time_lock
|
||||
.is_satisfied_by(max_chain_height, max_chain_mtp, max_utxo_height, max_utxo_mtp)
|
||||
.unwrap());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -163,14 +163,12 @@ struct Default {
|
|||
#[derive(Debug, Clone, PartialEq, Eq)] // All public types implement Debug (C-DEBUG).
|
||||
struct Errors {
|
||||
a: transaction::ParseOutPointError,
|
||||
b: relative::IncompatibleHeightError,
|
||||
c: relative::IncompatibleTimeError,
|
||||
d: relative::IncompatibleHeightError,
|
||||
e: relative::IncompatibleTimeError,
|
||||
f: relative::DisabledLockTimeError,
|
||||
g: relative::DisabledLockTimeError,
|
||||
h: script::RedeemScriptSizeError,
|
||||
i: script::WitnessScriptSizeError,
|
||||
b: relative::DisabledLockTimeError,
|
||||
c: relative::IsSatisfiedByError,
|
||||
d: relative::IsSatisfiedByHeightError,
|
||||
e: relative::IsSatisfiedByTimeError,
|
||||
f: script::RedeemScriptSizeError,
|
||||
g: script::WitnessScriptSizeError,
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -212,7 +210,7 @@ fn api_can_use_types_from_crate_root() {
|
|||
#[test]
|
||||
fn api_can_use_all_types_from_module_locktime() {
|
||||
use bitcoin_primitives::locktime::relative::{
|
||||
DisabledLockTimeError, IncompatibleHeightError, IncompatibleTimeError, LockTime,
|
||||
DisabledLockTimeError, InvalidHeightError, InvalidTimeError, LockTime,
|
||||
};
|
||||
use bitcoin_primitives::locktime::{absolute, relative};
|
||||
}
|
||||
|
|
|
@ -124,7 +124,7 @@ impl_u32_wrapper! {
|
|||
/// 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`].
|
||||
/// purpose block interval abstraction. For locktimes please see [`locktime::relative::NumberOfBlocks`].
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub struct BlockHeightInterval(u32);
|
||||
|
|
|
@ -83,6 +83,17 @@ impl Height {
|
|||
/// Converts this [`Height`] to a raw `u32` value.
|
||||
#[inline]
|
||||
pub const fn to_u32(self) -> u32 { self.0 }
|
||||
|
||||
/// Returns true if a transaction with this locktime can be included in the next block.
|
||||
///
|
||||
/// `self` is value of the `LockTime` and if `height` is the current chain tip then
|
||||
/// a transaction with this lock can be broadcast for inclusion in the next block.
|
||||
#[inline]
|
||||
pub fn is_satisfied_by(self, height: Height) -> bool {
|
||||
// Use u64 so that there can be no overflow.
|
||||
let next_block_height = u64::from(height.to_u32()) + 1;
|
||||
u64::from(self.to_u32()) <= next_block_height
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Height {
|
||||
|
@ -217,6 +228,18 @@ impl MedianTimePast {
|
|||
/// Converts this [`MedianTimePast`] to a raw `u32` value.
|
||||
#[inline]
|
||||
pub const fn to_u32(self) -> u32 { self.0 }
|
||||
|
||||
/// Returns true if a transaction with this locktime can be included in the next block.
|
||||
///
|
||||
/// `self`is the value of the `LockTime` and if `time` is the median time past of the block at
|
||||
/// the chain tip then a transaction with this lock can be broadcast for inclusion in the next
|
||||
/// block.
|
||||
#[inline]
|
||||
pub fn is_satisfied_by(self, time: MedianTimePast) -> bool {
|
||||
// The locktime check in Core during block validation uses the MTP
|
||||
// of the previous block - which is the expected to be `time` here.
|
||||
self <= time
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for MedianTimePast {
|
||||
|
@ -326,7 +349,7 @@ impl std::error::Error for ConversionError {
|
|||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None }
|
||||
}
|
||||
|
||||
/// Describes the two types of locking, lock-by-blockheight and lock-by-blocktime.
|
||||
/// Describes the two types of locking, lock-by-height and lock-by-time.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
enum LockTimeUnit {
|
||||
/// Lock by blockheight.
|
||||
|
@ -341,9 +364,9 @@ impl fmt::Display for LockTimeUnit {
|
|||
|
||||
match *self {
|
||||
L::Blocks =>
|
||||
write!(f, "expected lock-by-blockheight (must be < {})", LOCK_TIME_THRESHOLD),
|
||||
write!(f, "expected lock-by-height (must be < {})", LOCK_TIME_THRESHOLD),
|
||||
L::Seconds =>
|
||||
write!(f, "expected lock-by-blocktime (must be >= {})", LOCK_TIME_THRESHOLD),
|
||||
write!(f, "expected lock-by-time (must be >= {})", LOCK_TIME_THRESHOLD),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -553,8 +576,8 @@ mod tests {
|
|||
let blocks = LockTimeUnit::Blocks;
|
||||
let seconds = LockTimeUnit::Seconds;
|
||||
|
||||
assert_eq!(format!("{}", blocks), "expected lock-by-blockheight (must be < 500000000)");
|
||||
assert_eq!(format!("{}", seconds), "expected lock-by-blocktime (must be >= 500000000)");
|
||||
assert_eq!(format!("{}", blocks), "expected lock-by-height (must be < 500000000)");
|
||||
assert_eq!(format!("{}", seconds), "expected lock-by-time (must be >= 500000000)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -14,7 +14,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
|||
#[doc(hidden)]
|
||||
pub type Height = NumberOfBlocks;
|
||||
|
||||
/// A relative lock time lock-by-blockheight value.
|
||||
/// A relative lock time lock-by-height value.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct NumberOfBlocks(u16);
|
||||
|
||||
|
@ -54,29 +54,27 @@ impl NumberOfBlocks {
|
|||
self.0 as u32 // cast safety: u32 is wider than u16 on all architectures
|
||||
}
|
||||
|
||||
/// Determines whether a relative‐height locktime has matured, taking into account
|
||||
/// both the chain tip and the height at which the UTXO was confirmed.
|
||||
/// Returns true if an output locked by height can be spent in the next block.
|
||||
///
|
||||
/// If you have two height intervals `x` and `y`, and want to know whether `x`
|
||||
/// is satisfied by `y`, use `x >= y`.
|
||||
/// # Errors
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `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`.
|
||||
/// If `chain_tip` as not _after_ `utxo_mined_at` i.e., if you get the args mixed up.
|
||||
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)
|
||||
) -> Result<bool, InvalidHeightError> {
|
||||
match chain_tip.checked_sub(utxo_mined_at) {
|
||||
Some(diff) => {
|
||||
if diff.to_u32() == u32::MAX {
|
||||
// Weird but ok none the less - protects against overflow below.
|
||||
return Ok(true);
|
||||
}
|
||||
// +1 because the next block will have height 1 higher than `chain_tip`.
|
||||
Ok(u32::from(self.to_height()) <= diff.to_u32() + 1)
|
||||
}
|
||||
None => Err(InvalidHeightError { chain_tip, utxo_mined_at }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -117,9 +115,9 @@ impl<'de> Deserialize<'de> for NumberOfBlocks {
|
|||
#[doc(hidden)]
|
||||
pub type Time = NumberOf512Seconds;
|
||||
|
||||
/// A relative lock time lock-by-blocktime value.
|
||||
/// A relative lock time lock-by-time value.
|
||||
///
|
||||
/// For BIP 68 relative lock-by-blocktime locks, time is measured in 512 second intervals.
|
||||
/// For BIP 68 relative lock-by-time locks, time is measured in 512 second intervals.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct NumberOf512Seconds(u16);
|
||||
|
||||
|
@ -133,7 +131,8 @@ impl NumberOf512Seconds {
|
|||
/// The maximum relative block time (33,554,432 seconds or approx 388 days).
|
||||
pub const MAX: Self = NumberOf512Seconds(u16::MAX);
|
||||
|
||||
/// Constructs a new [`NumberOf512Seconds`] using time intervals where each interval is equivalent to 512 seconds.
|
||||
/// Constructs a new [`NumberOf512Seconds`] using time intervals where each interval is
|
||||
/// equivalent to 512 seconds.
|
||||
///
|
||||
/// Encoding finer granularity of time for relative lock-times is not supported in Bitcoin.
|
||||
#[inline]
|
||||
|
@ -144,8 +143,8 @@ impl NumberOf512Seconds {
|
|||
#[must_use]
|
||||
pub const fn to_512_second_intervals(self) -> u16 { self.0 }
|
||||
|
||||
/// Constructs a new [`NumberOf512Seconds`] from seconds, converting the seconds into 512 second
|
||||
/// interval with truncating division.
|
||||
/// Constructs a new [`NumberOf512Seconds`] from seconds, converting the seconds into a 512
|
||||
/// second interval using truncating division.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
|
@ -161,8 +160,8 @@ impl NumberOf512Seconds {
|
|||
}
|
||||
}
|
||||
|
||||
/// Constructs a new [`NumberOf512Seconds`] from seconds, converting the seconds into 512 second
|
||||
/// intervals with ceiling division.
|
||||
/// Constructs a new [`NumberOf512Seconds`] from seconds, converting the seconds into a 512
|
||||
/// second interval using ceiling division.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
|
@ -201,29 +200,24 @@ impl NumberOf512Seconds {
|
|||
(1u32 << 22) | self.0 as u32 // cast safety: u32 is wider than u16 on all architectures
|
||||
}
|
||||
|
||||
/// Determines whether a relative‑time lock has matured, taking into account both
|
||||
/// the UTXO’s Median Time Past at confirmation and the required delay.
|
||||
/// Returns true if an output locked by time can be spent in the next block.
|
||||
///
|
||||
/// If you have two MTP intervals `x` and `y`, and want to know whether `x`
|
||||
/// is satisfied by `y`, use `x >= y`.
|
||||
/// # Errors
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `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
|
||||
/// If `chain_tip` as not _after_ `utxo_mined_at` i.e., if you get the args mixed up.
|
||||
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)
|
||||
) -> Result<bool, InvalidTimeError> {
|
||||
match chain_tip.checked_sub(utxo_mined_at) {
|
||||
Some(diff) => {
|
||||
// The locktime check in Core during block validation uses the MTP of the previous
|
||||
// block - which is `chain_tip` here.
|
||||
Ok(self.to_seconds() <= diff.to_u32())
|
||||
}
|
||||
None => Err(InvalidTimeError { chain_tip, utxo_mined_at }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -288,6 +282,44 @@ impl fmt::Display for TimeOverflowError {
|
|||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for TimeOverflowError {}
|
||||
|
||||
/// Error returned when `NumberOfBlocks::is_satisfied_by` is incorrectly called.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct InvalidHeightError {
|
||||
/// The `chain_tip` argument.
|
||||
pub(crate) chain_tip: crate::BlockHeight,
|
||||
/// The `utxo_mined_at` argument.
|
||||
pub(crate) utxo_mined_at: crate::BlockHeight,
|
||||
}
|
||||
|
||||
impl fmt::Display for InvalidHeightError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "is_satisfied_by arguments invalid (probably the wrong way around) chain_tip: {} utxo_mined_at: {}", self.chain_tip, self.utxo_mined_at
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for InvalidHeightError {}
|
||||
|
||||
/// Error returned when `NumberOf512Seconds::is_satisfied_by` is incorrectly called.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct InvalidTimeError {
|
||||
/// The `chain_tip` argument.
|
||||
pub(crate) chain_tip: crate::BlockMtp,
|
||||
/// The `utxo_mined_at` argument.
|
||||
pub(crate) utxo_mined_at: crate::BlockMtp,
|
||||
}
|
||||
|
||||
impl fmt::Display for InvalidTimeError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "is_satisfied_by arguments invalid (probably the wrong way around) chain_tip: {} utxo_mined_at: {}", self.chain_tip, self.utxo_mined_at
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for InvalidTimeError {}
|
||||
|
||||
#[cfg(feature = "arbitrary")]
|
||||
impl<'a> Arbitrary<'a> for NumberOfBlocks {
|
||||
fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
|
||||
|
@ -426,24 +458,24 @@ mod tests {
|
|||
let time_lock = NumberOf512Seconds::from_512_second_intervals(10);
|
||||
let chain_state1 = BlockMtp::new(timestamps);
|
||||
let utxo_state1 = BlockMtp::new(utxo_timestamps);
|
||||
assert!(time_lock.is_satisfied_by(chain_state1, utxo_state1));
|
||||
assert!(time_lock.is_satisfied_by(chain_state1, utxo_state1).unwrap());
|
||||
|
||||
// Test case 2: Not satisfied (current_mtp < utxo_mtp + required_seconds)
|
||||
let chain_state2 = BlockMtp::new(timestamps2);
|
||||
let utxo_state2 = BlockMtp::new(utxo_timestamps2);
|
||||
assert!(!time_lock.is_satisfied_by(chain_state2, utxo_state2));
|
||||
assert!(!time_lock.is_satisfied_by(chain_state2, utxo_state2).unwrap());
|
||||
|
||||
// Test case 3: Test with a larger value (100 intervals = 51200 seconds)
|
||||
let larger_lock = NumberOf512Seconds::from_512_second_intervals(100);
|
||||
let chain_state3 = BlockMtp::new(timestamps3);
|
||||
let utxo_state3 = BlockMtp::new(utxo_timestamps3);
|
||||
assert!(larger_lock.is_satisfied_by(chain_state3, utxo_state3));
|
||||
assert!(larger_lock.is_satisfied_by(chain_state3, utxo_state3).unwrap());
|
||||
|
||||
// Test case 4: Overflow handling - tests that is_satisfied_by handles overflow gracefully
|
||||
let max_time_lock = NumberOf512Seconds::MAX;
|
||||
let chain_state4 = BlockMtp::new(timestamps);
|
||||
let utxo_state4 = BlockMtp::new(utxo_timestamps);
|
||||
assert!(!max_time_lock.is_satisfied_by(chain_state4, utxo_state4));
|
||||
assert!(!max_time_lock.is_satisfied_by(chain_state4, utxo_state4).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -453,19 +485,19 @@ mod tests {
|
|||
let height_lock = NumberOfBlocks(10);
|
||||
|
||||
// Test case 1: Satisfaction (current_height >= utxo_height + required)
|
||||
let chain_state1 = BlockHeight::from_u32(100);
|
||||
let chain_state1 = BlockHeight::from_u32(89);
|
||||
let utxo_state1 = BlockHeight::from_u32(80);
|
||||
assert!(height_lock.is_satisfied_by(chain_state1, utxo_state1));
|
||||
assert!(height_lock.is_satisfied_by(chain_state1, utxo_state1).unwrap());
|
||||
|
||||
// Test case 2: Not satisfied (current_height < utxo_height + required)
|
||||
let chain_state2 = BlockHeight::from_u32(89);
|
||||
let chain_state2 = BlockHeight::from_u32(88);
|
||||
let utxo_state2 = BlockHeight::from_u32(80);
|
||||
assert!(!height_lock.is_satisfied_by(chain_state2, utxo_state2));
|
||||
assert!(!height_lock.is_satisfied_by(chain_state2, utxo_state2).unwrap());
|
||||
|
||||
// Test case 3: Overflow handling - tests that is_satisfied_by handles overflow gracefully
|
||||
let max_height_lock = NumberOfBlocks::MAX;
|
||||
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));
|
||||
assert!(!max_height_lock.is_satisfied_by(chain_state3, utxo_state3).unwrap());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -139,11 +139,13 @@ struct Errors {
|
|||
x: locktime::absolute::ConversionError,
|
||||
y: locktime::absolute::Height,
|
||||
z: locktime::absolute::ParseHeightError,
|
||||
_a: locktime::absolute::ParseTimeError,
|
||||
_b: locktime::relative::TimeOverflowError,
|
||||
_e: parse::ParseIntError,
|
||||
_f: parse::PrefixedHexError,
|
||||
_g: parse::UnprefixedHexError,
|
||||
aa: locktime::absolute::ParseTimeError,
|
||||
ab: locktime::relative::TimeOverflowError,
|
||||
ac: locktime::relative::InvalidHeightError,
|
||||
ad: locktime::relative::InvalidTimeError,
|
||||
ae: parse::ParseIntError,
|
||||
af: parse::PrefixedHexError,
|
||||
ag: parse::UnprefixedHexError,
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
Loading…
Reference in New Issue