Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
4 changed files
with
267 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} | ||
} |