-
-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
618a8ad
commit 5986927
Showing
4 changed files
with
295 additions
and
18 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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,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)) | ||
} | ||
} | ||
} |
Oops, something went wrong.