Skip to content

Commit

Permalink
wip on timelocks
Browse files Browse the repository at this point in the history
  • Loading branch information
dr-orlovsky committed Oct 2, 2023
1 parent 618a8ad commit 5986927
Show file tree
Hide file tree
Showing 4 changed files with 295 additions and 18 deletions.
32 changes: 16 additions & 16 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 18 additions & 2 deletions psbt/src/maps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@

use bp::{
ComprPubkey, Descriptor, KeyOrigin, LegacyPubkey, LockTime, NormalIndex, Outpoint,
RedeemScript, Sats, ScriptPubkey, SeqNo, SigScript, Terminal, Tx, TxOut, TxVer, Txid, Vout,
Witness, WitnessScript, Xpub, XpubOrigin,
RedeemScript, Sats, ScriptPubkey, SeqNo, SigScript, Terminal, Tx, TxOut, TxVer, Txid, VarInt,
Vout, Weight, WeightUnits, Witness, WitnessScript, Xpub, XpubOrigin,
};
use indexmap::IndexMap;

Expand Down Expand Up @@ -239,6 +239,22 @@ impl Psbt {
}
}

impl Weight for Psbt {
fn weight_units(&self) -> WeightUnits {
let bytes = 4 // version
+ VarInt::with(self.inputs.len()).len()
+ VarInt::with(self.outputs.len()).len()
+ 4; // lock time
let mut weight = WeightUnits::no_discount(bytes)
+ self.inputs().map(TxIn::weight_units).sum()
+ self.outputs().map(TxOut::weight_units).sum();
if self.is_segwit() {
weight += WeightUnits::witness_discount(2); // marker and flag bytes
}
weight
}
}

#[derive(Clone, Eq, PartialEq, Debug)]
#[cfg_attr(
feature = "serde",
Expand Down
224 changes: 224 additions & 0 deletions std/src/seq.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
// Modern, minimalistic & standard-compliant cold wallet library.
//
// SPDX-License-Identifier: Apache-2.0
//
// Written in 2020-2023 by
// Dr Maxim Orlovsky <orlovsky@lnp-bp.org>
//
// Copyright (C) 2020-2023 LNP/BP Standards Association. All rights reserved.
// Copyright (C) 2020-2023 Dr Maxim Orlovsky. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

pub const SEQ_NO_MAX_VALUE: u32 = 0xFFFFFFFF;
pub const SEQ_NO_SUBMAX_VALUE: u32 = 0xFFFFFFFE;
pub const SEQ_NO_CSV_DISABLE_MASK: u32 = 0x80000000;
pub const SEQ_NO_CSV_TYPE_MASK: u32 = 0x00400000;

pub trait SeqNoExt {
/// Classifies type of `nSeq` value (see [`SeqNoClass`]).
#[inline]
fn classify(self) -> SeqNoClass;

/// Checks if `nSeq` value opts-in for replace-by-fee (also always true for
/// relative time locks).
#[inline]
fn is_rbf(self) -> bool;

/// Checks if `nSeq` value opts-in for relative time locks (also always
/// imply RBG opt-in).
#[inline]
fn is_timelock(self) -> bool;
}

impl SeqNoExt for SeqNo {
#[inline]
fn classify(self) -> SeqNoClass {
match self.0 {
SEQ_NO_MAX_VALUE | SEQ_NO_SUBMAX_VALUE => SeqNoClass::Unencumbered,
no if no & SEQ_NO_CSV_DISABLE_MASK != 0 => SeqNoClass::RbfOnly,
no if no & SEQ_NO_CSV_TYPE_MASK != 0 => SeqNoClass::RelativeTime,
_ => SeqNoClass::RelativeHeight,
}
}

#[inline]
fn is_rbf(self) -> bool { self.0 < SEQ_NO_SUBMAX_VALUE }

#[inline]
fn is_timelock(self) -> bool { self.0 & SEQ_NO_CSV_DISABLE_MASK > 1 }
}

/// Classes for `nSeq` values
#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)]
pub enum SeqNoClass {
/// No RBF (opt-out) and timelocks.
///
/// Corresponds to `0xFFFFFFFF` and `0xFFFFFFFE` values
Unencumbered,

/// RBF opt-in, but no timelock applied.
///
/// Values from `0x80000000` to `0xFFFFFFFD` inclusively
RbfOnly,

/// Both RBF and relative height-based lock is applied.
RelativeTime,

/// Both RBF and relative time-based lock is applied.
RelativeHeight,
}

#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub struct Rbf(SeqNo);

impl Rbf {
/// Creates `nSeq` value which is not encumbered by either RBF not relative
/// time locks.
///
/// # Arguments
/// - `max` defines whether `nSeq` should be set to the `0xFFFFFFFF` (`true`) or `0xFFFFFFFe`.
#[inline]
pub fn unencumbered(max: bool) -> SeqNo {
SeqNo(if max { SEQ_NO_MAX_VALUE } else { SEQ_NO_SUBMAX_VALUE })
}

/// Creates `nSeq` in replace-by-fee mode with the specified order number.
#[inline]
pub fn from_rbf(order: u16) -> SeqNo { SeqNo(order as u32 | SEQ_NO_CSV_DISABLE_MASK) }

/// Creates `nSeq` in replace-by-fee mode with value 0xFFFFFFFD.
///
/// This value is the value supported by the BitBox software.
#[inline]
pub fn rbf() -> SeqNo { SeqNo(SEQ_NO_SUBMAX_VALUE - 1) }
}

pub struct RelativeLock(SeqNo);

impl RelativeLock {
/// Creates relative time lock measured in number of blocks (implies RBF).
#[inline]
pub fn from_height(blocks: u16) -> SeqNo { SeqNo(blocks as u32) }

/// Creates relative time lock measured in number of 512-second intervals
/// (implies RBF).
#[inline]
pub fn from_intervals(intervals: u16) -> SeqNo {
SeqNo(intervals as u32 | SEQ_NO_CSV_TYPE_MASK)
}

/// Gets structured relative time lock information from the `nSeq` value.
/// See [`TimeLockInterval`].
pub fn time_lock_interval(self) -> Option<TimeLockInterval> {
if self.0 & SEQ_NO_CSV_DISABLE_MASK != 0 {
None
} else if self.0 & SEQ_NO_CSV_TYPE_MASK != 0 {
Some(TimeLockInterval::Time((self.0 & 0xFFFF) as u16))
} else {
Some(TimeLockInterval::Height((self.0 & 0xFFFF) as u16))
}
}
}

#[derive(Debug, Clone, PartialEq, Eq, From, Display)]
#[display(doc_comments)]
pub enum ParseError {
/// invalid number in time lock descriptor
#[from]
InvalidNumber(ParseIntError),

/// block height `{0}` is too large for time lock
InvalidHeight(u32),

/// timestamp `{0}` is too small for time lock
InvalidTimestamp(u32),

/// time lock descriptor `{0}` is not recognized
InvalidDescriptor(String),

/// use of randomly-generated RBF sequence numbers requires compilation
/// with `rand` feature
NoRand,
}

impl std::error::Error for ParseError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ParseError::InvalidNumber(err) => Some(err),
_ => None,
}
}
}

impl Display for Rbf {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self.classify() {
SeqNoClass::Unencumbered if self.0 == SEQ_NO_MAX_VALUE => {
f.write_str("final(0xFFFFFFFF)")
}
SeqNoClass::Unencumbered if self.0 == SEQ_NO_SUBMAX_VALUE => {
f.write_str("non-rbf(0xFFFFFFFE)")
}
SeqNoClass::Unencumbered => unreachable!(),
SeqNoClass::RbfOnly => {
f.write_str("rbf(")?;
Display::fmt(&(self.0 ^ SEQ_NO_CSV_DISABLE_MASK), f)?;
f.write_str(")")
}
_ if self.0 >> 16 & 0xFFBF > 0 => Display::fmt(&self.0, f),
SeqNoClass::RelativeTime => {
let value = self.0 & 0xFFFF;
f.write_str("time(")?;
Display::fmt(&value, f)?;
f.write_str(")")
}
SeqNoClass::RelativeHeight => {
let value = self.0 & 0xFFFF;
f.write_str("height(")?;
Display::fmt(&value, f)?;
f.write_str(")")
}
}
}
}

impl FromStr for Rbf {
type Err = ParseError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.to_lowercase();
if s == "rbf" {
#[cfg(feature = "rand")]
{
Ok(SeqNo::rbf())
}
#[cfg(not(feature = "rand"))]
{
Err(ParseError::NoRand)
}
} else if s.starts_with("rbf(") && s.ends_with(')') {
let no = s[4..].trim_end_matches(')').parse()?;
Ok(SeqNo::from_rbf(no))
} else if s.starts_with("time(") && s.ends_with(')') {
let no = s[5..].trim_end_matches(')').parse()?;
Ok(SeqNo::from_intervals(no))
} else if s.starts_with("height(") && s.ends_with(')') {
let no = s[7..].trim_end_matches(')').parse()?;
Ok(SeqNo::from_height(no))
} else {
let no = s.parse()?;
Ok(SeqNo(no))
}
}
}
Loading

0 comments on commit 5986927

Please sign in to comment.