From e766192ab710df723282aef39014e79e4113d8bd Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Tue, 17 May 2022 06:27:31 +1000 Subject: [PATCH] Add timelock module Bitcoin supports various timelocks based on the nLockTime and nSequence numbers. Absolute timelocks are defined in BIP-65 and relative timelocks are defined in BIP-112 and BIP-68. Add a timelock module to support the various Bitcoin timelocks. --- src/blockdata/constants.rs | 19 +++ src/lib.rs | 1 + src/util/mod.rs | 1 + src/util/timelock.rs | 236 +++++++++++++++++++++++++++++++++++++ 4 files changed, 257 insertions(+) create mode 100644 src/util/timelock.rs diff --git a/src/blockdata/constants.rs b/src/blockdata/constants.rs index 4ad8079ef2..74b3edbd05 100644 --- a/src/blockdata/constants.rs +++ b/src/blockdata/constants.rs @@ -64,6 +64,25 @@ pub const MAX_SCRIPT_ELEMENT_SIZE: usize = 520; /// How may blocks between halvings. pub const SUBSIDY_HALVING_INTERVAL: u32 = 210_000; +/// The Threshold for deciding whether `nLockTime` is interpreted as time or height. +/// https://github.com/bitcoin/bitcoin/blob/9ccaee1d5e2e4b79b0a7c29aadb41b97e4741332/src/script/script.h#L39 +pub const LOCKTIME_THRESHOLD: u32 = 500_000_000; + +/// Bit flag for deciding whether sequence number is interpreted as height or time. If nSequence +/// encodes a relative lock-time and this flag is set, the relative lock-time has units of 512 +/// seconds, otherwise it specifies blocks with a granularity of 1. +/// https://github.com/bitcoin/bips/blob/master/bip-0112.mediawiki +pub const SEQUENCE_LOCKTIME_TYPE_FLAG: u32 = 1 << 22; + +/// Disable flag for sequence locktime. Applies in the context of BIP-68. If this flag set, +/// nSequence is NOT interpreted as a relative lock-time. For future soft-fork compatibility. +/// https://github.com/bitcoin/bips/blob/master/bip-0112.mediawiki +pub const SEQUENCE_LOCKTIME_DISABLE_FLAG: u32 = 1 << 31; + +/// Granularity for time-based relative lock-time is fixed at 512 seconds, equivalent to 2^9. +/// https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki +pub const SEQUENCE_LOCKTIME_GRANULARITY: u32 = 9; + /// In Bitcoind this is insanely described as ~((u256)0 >> 32) pub fn max_target(_: Network) -> Uint256 { Uint256::from_u64(0xFFFF).unwrap() << 208 diff --git a/src/lib.rs b/src/lib.rs index 0b7bc883dc..4f9571e32d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -135,6 +135,7 @@ pub use util::amount::Denomination; pub use util::amount::SignedAmount; pub use util::merkleblock::MerkleBlock; pub use util::sighash::SchnorrSighashType; +pub use util::timelock; pub use util::ecdsa::{self, EcdsaSig, EcdsaSigError}; pub use util::schnorr::{self, SchnorrSig, SchnorrSigError}; diff --git a/src/util/mod.rs b/src/util/mod.rs index bace2e45bc..6c96b1ab05 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -33,6 +33,7 @@ pub mod taproot; pub mod uint; pub mod bip158; pub mod sighash; +pub mod timelock; pub(crate) mod endian; diff --git a/src/util/timelock.rs b/src/util/timelock.rs new file mode 100644 index 0000000000..110cb29352 --- /dev/null +++ b/src/util/timelock.rs @@ -0,0 +1,236 @@ +// Rust Bitcoin Library +// Written in 2022 by +// Tobin C. Harding +// To the extent possible under law, the author(s) have dedicated all +// copyright and related and neighboring rights to this software to +// the public domain worldwide. This software is distributed without +// any warranty. +// +// You should have received a copy of the CC0 Public Domain Dedication +// along with this software. +// If not, see . +// + +//! Bitcoin timelocks. +//! +//! Relative and absolute timelocks based on `nSequence` and `nLockTime`. +//! + +#[cfg(feature = "std")] +use std::error; + +use core::fmt; + +use blockdata::constants::{ + LOCKTIME_THRESHOLD, SEQUENCE_LOCKTIME_DISABLE_FLAG, SEQUENCE_LOCKTIME_GRANULARITY, + SEQUENCE_LOCKTIME_TYPE_FLAG, +}; + +/// An absolute time lock, expires after either height or time. +/// +/// This is nLockTime as defined in BIP-65. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Abs(u32); + +impl Abs { + /// Returns true if this absolute lock is lock-by-blockheight. + pub fn is_block_time(self) -> bool { + self.0 < LOCKTIME_THRESHOLD + } + + /// Returns true if this absolute lock is lock-by-blocktime. + pub fn is_block_height(self) -> bool { + self.0 >= LOCKTIME_THRESHOLD + } + + /// Returns true if this timelock is the same type as `other` timelock. + pub fn is_same_type(&self, other: Self) -> bool { + self.is_block_time() && other.is_block_time() + || self.is_block_height() && other.is_block_height() + } + + /// Returns true if the timelock is locked at `age`. + pub fn is_locked(&self, age: u32) -> Result { + self.is_expired(age).map(|b| !b) + } + + /// Returns true if the timelock has expired at `age`. + pub fn is_expired(&self, age: u32) -> Result { + let age = Abs::from(age); + if !self.is_same_type(age) { + return Err(Error::InvalidComparison); + } + Ok(age.0 >= self.0) + } + + /// Returns true if this timelock value is zero. + pub fn is_zero(&self) -> bool { + self.0 == 0 + } + + /// Returns the nLockTime value. This is the inverse of `From`. + /// + /// # Examples + /// ``` + /// # use miniscript::miniscript::timelock::Abs; + /// let n = 0xdeadbeef; + /// let lock = Abs::from(n); + /// assert_eq!(lock.n_lock_time(), n); + ///``` + pub fn n_lock_time(&self) -> u32 { + self.to_u32() + } + + /// Returns the time lock value. This is the inverse of `From`. + // FIXME: Should we provide this method as well as n_lock_time() or just one of them? + pub fn to_u32(&self) -> u32 { + self.0 + } +} + +impl From for Abs { + /// Creates an absolute timelock from nLockTime. + fn from(n_lock_time: u32) -> Self { + Abs(n_lock_time) + } +} + +impl fmt::Display for Abs { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +/// Relative time lock, either after n blocks or after duration (time) as defined by BIP-112. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Rel(u32); + +const SEQUENCE_LOCKTIME_MASK: u32 = 0x0000ffff; + +impl Rel { + /// Returns true if this timelock is blocks-based. + pub fn is_block_based(&self) -> bool { + !self.is_time_based() + } + + /// Returns true if this timelock is time-based. This means the relative + /// timelock has units of 512 seconds. + pub fn is_time_based(&self) -> bool { + self.0 & SEQUENCE_LOCKTIME_TYPE_FLAG != 0 + } + + /// Returns true if this timelock is the same type as `other` timelock. + pub fn is_same_type(&self, other: Self) -> bool { + self.is_time_based() && other.is_time_based() + || self.is_block_based() && other.is_block_based() + } + + /// Returns true if nSequence is NOT interpreted as a relative timelock. + pub fn is_disabled(&self) -> bool { + self.0 & SEQUENCE_LOCKTIME_DISABLE_FLAG != 0 + } + + /// Returns true if the timelock is locked at `age`. + pub fn is_locked(&self, age: u32) -> Result { + self.is_expired(age).map(|b| !b) + } + + /// Returns true if the timelock has expired at `age`. + pub fn is_expired(&self, age: u32) -> Result { + let age = Rel::from(age); + + // FIXME: Do we need to do this check or should call be required to do this? + if self.is_disabled() || age.is_disabled() { + return Err(Error::Disabled); + } + + if !self.is_same_type(age) { + return Err(Error::InvalidComparison); + } + + Ok(age.value() >= self.value()) + } + + /// Returns true if this timelock value is zero. + pub fn is_zero(&self) -> bool { + self.value() == 0 + } + + /// Returns the value of the relative time lock. Note, this is not the + /// original nSequence number but rather the masked 16 bit value. + pub fn value(&self) -> u16 { + (self.0 & SEQUENCE_LOCKTIME_MASK) as u16 + } + + /// Converts a relative time-based timelock to seconds. + pub fn seconds(&self) -> Result { + if !self.is_time_based() { + return Err(Error::NotTimeBased); + } + // 2^9 because units encode 512 second granularity (see BIP-68). + Ok((self.value() as u32) << SEQUENCE_LOCKTIME_GRANULARITY) + } + + /// Returns the nSequence value. This is the inverse of `From`. + /// + /// # Examples + /// ``` + /// # use miniscript::miniscript::timelock::Rel; + /// let n = 0xdeadbeef; + /// let lock = Rel::from(n); + /// assert_eq!(lock.n_sequence(), n); + ///``` + pub fn n_sequence(&self) -> u32 { + self.0 + } + + /// Returns the time lock value. This is the inverse of `From`. + // FIXME: Should we provide this method as well as n_sequence() or just one of them? + pub fn to_u32(&self) -> u32 { + self.0 + } +} + +impl From for Rel { + /// Creates a relative timelock from nSequence. + fn from(n_sequence: u32) -> Self { + Rel(n_sequence) + } +} + +impl fmt::Display for Rel { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +/// Timelock related errors. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Error { + /// Cannot compare different type locks (time vs height/block). + InvalidComparison, + /// Timelock is not a time based lock. + NotTimeBased, + /// Timelock is disabled. + Disabled, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use self::Error::*; + + match self { + InvalidComparison => f.write_str("cannot compare different type locks (time vs height/block)"), + NotTimeBased => f.write_str("timelock is not a time based lock"), + Disabled => f.write_str("timelock is disabled"), + } + } +} + +#[cfg(feature = "std")] +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] +impl error::Error for Error { + fn cause(&self) -> Option<&dyn error::Error> { + None + } +}