Skip to content

Commit

Permalink
Add timelock module
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
tcharding committed May 19, 2022
1 parent 50d9394 commit 52f19da
Show file tree
Hide file tree
Showing 4 changed files with 267 additions and 0 deletions.
19 changes: 19 additions & 0 deletions src/blockdata/constants.rs
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Expand Up @@ -127,6 +127,7 @@ pub use crate::blockdata::transaction::EcdsaSighashType;
pub use crate::blockdata::witness::Witness;
pub use crate::consensus::encode::VarInt;
pub use crate::network::constants::Network;
pub use crate::util::timelock;
pub use crate::util::Error;
pub use crate::util::address::Address;
pub use crate::util::address::AddressType;
Expand Down
1 change: 1 addition & 0 deletions src/util/mod.rs
Expand Up @@ -33,6 +33,7 @@ pub mod taproot;
pub mod uint;
pub mod bip158;
pub mod sighash;
pub mod timelock;

pub(crate) mod endian;

Expand Down
246 changes: 246 additions & 0 deletions src/util/timelock.rs
@@ -0,0 +1,246 @@
// Rust Bitcoin Library
// Written in 2022 by
// Tobin C. Harding <me@tobin.cc>
// 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 <http://creativecommons.org/publicdomain/zero/1.0/>.
//

//! Bitcoin timelocks.
//!
//! Relative and absolute timelocks based on `nSequence` and `nLockTime`.
//!

#[cfg(feature = "std")]
use std::error;

use core::fmt;

use crate::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 this timelock is locked at the specific blocktime/blockheight `age`.
///
/// # Errors
///
/// If this timelock and `age` are different types i.e., one represents a
/// block height and the other represents a block time.
pub fn is_locked(&self, age: u32) -> Result<bool, Error> {
self.is_expired(age).map(|b| !b)
}

/// Returns true if this timelock is expired at the specific blocktime/blockheight `age`.
///
/// # Errors
///
/// If this timelock and `age` are different types i.e., one represents a
/// block height and the other represents a block time.
pub fn is_expired(&self, age: u32) -> Result<bool, Error> {
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.
///
/// A zero value means that there is no timelock i.e., a transaction with a
/// zero timelock can be included immediately in the next block.
pub fn is_zero(&self) -> bool {
self.0 == 0
}

/// Returns the original nLockTime value.
pub fn to_u32(&self) -> u32 {
self.0
}
}

impl From<u32> 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 this timelock is locked at the specific block/time `age`.
///
/// # Errors
///
/// - If this timelock and `age` are different types i.e., one represents a
/// block and the other represents a time.
/// - If either this timelock or `age` has the disabled bit is set.
pub fn is_locked(&self, age: u32) -> Result<bool, Error> {
self.is_expired(age).map(|b| !b)
}

/// Returns true if this timelock is expired at the specific block/time `age`.
///
/// # Errors
///
/// - If this timelock and `age` are different types i.e., one represents a
/// block and the other represents a time.
/// - If either this timelock or `age` has the disabled bit is set.
pub fn is_expired(&self, age: u32) -> Result<bool, Error> {
let age = Rel::from(age);

// It is not clear what it means to compare timelocks and age values if either has the
// disable bit set. Users of `is_expired` should probably be checking for this bit before
// calling `is_expired`. If you hit this error it is most likely a bug in your
// implementation.
if self.is_disabled() {
return Err(Error::TimelockHasDisableBitSet);
}
if age.is_disabled() {
return Err(Error::AgeHasDisableBitSet);
}

if !self.is_same_type(age) {
return Err(Error::InvalidComparison);
}

Ok(age.value() >= self.value())
}

/// Returns true if this timelock value is zero.
///
/// A zero value means that a transaction with this timelock can be included in any block.
/// This is useful, for example, for chaining unconfirmed transactions off of each other.
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<u32, Error> {
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 original nSequence value.
pub fn to_u32(&self) -> u32 {
self.0
}
}

impl From<u32> 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,
/// Attempted to check expiry of a relative timelock that had the disable bit set.
TimelockHasDisableBitSet,
/// Attempted to check expiry of a relative timelock against an age value
/// that had the disable bit set.
AgeHasDisableBitSet,
}

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"),
TimelockHasDisableBitSet => f.write_str("attempted to check expiry of a relative timelock that had the disable bit set"),
AgeHasDisableBitSet => f.write_str("attempted to check expiry of a relative timelock against an age value that had the disable bit set"),
}
}
}

#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
impl error::Error for Error {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
None
}
}

0 comments on commit 52f19da

Please sign in to comment.