Skip to content

Add relative lock time type#1196

Merged
apoelstra merged 3 commits intorust-bitcoin:masterfrom
tcharding:08-16-relative-lock-time
Sep 1, 2022
Merged

Add relative lock time type#1196
apoelstra merged 3 commits intorust-bitcoin:masterfrom
tcharding:08-16-relative-lock-time

Conversation

@tcharding
Copy link
Copy Markdown
Member

@tcharding tcharding commented Aug 16, 2022

Patches 1 and 2 are preparation. Patch 3 adds a new relative::LockTime type.

Please see rust-bitcoin/rust-miniscript#455 for demo of use of the new API in miniscript.

Some links to help during review

@tcharding tcharding force-pushed the 08-16-relative-lock-time branch 2 times, most recently from 49ef7b6 to ac14bd4 Compare August 16, 2022 04:07
Copy link
Copy Markdown
Contributor

@nlanson nlanson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quick review.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docs are clear but naming might be a little confusing.
Just from the name I would assume that this returns the underlying u32 of Sequence, not just the low 16 bits.

Maybe timelock_bits()?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or low_u16()

Copy link
Copy Markdown
Member Author

@tcharding tcharding Aug 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like low_u16, cheers.

@tcharding tcharding force-pushed the 08-16-relative-lock-time branch from ac14bd4 to 67c9d2e Compare August 17, 2022 06:17
@tcharding
Copy link
Copy Markdown
Member Author

Implemented suggestions above and rebased.

In preparation for adding a relative lock time type move the `locktime`
module to a new module called `absolute`. Use qualified path for
locktime types (e.g. `absolute::PackedLockTime`) to improve readability.
Bitcoin lock times are easy to get confused, add absolute/relative in
various places to help clarify things.
@tcharding tcharding force-pushed the 08-16-relative-lock-time branch from 67c9d2e to 2e6a8ae Compare August 24, 2022 05:19
@tcharding tcharding marked this pull request as ready for review August 24, 2022 05:19
@tcharding
Copy link
Copy Markdown
Member Author

Rebased on top of 'disable fuzzer' patch, slightly improved docs.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In 2e6a8aefc048c28f71d8a4efb5f1d2766354c9f2:

I don't think it makes sense for a locktime to be satisfied by a sequence no, or vice-versa. Rather, I would expect a sequence number to be convertable to a relative::Locktime, and then this would be satisfied by a blockheight and/or timestamp.

IOW I would replace this method with one that takes a height and a time, and then just passes through to is_satisfied_by_height() && is_satisfied_by_time().

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, if you keep the existing false returns (see below) then && should be replaced by ||.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still not fully understanding where the height and time values would come from to pass into this new function. Would a user of this function have access to the chain and be expected to be able to compute the current height and time? I've based this (and the absolute::LockTime work) off of the usage in miniscript, is it that miniscript is not a normal user? How would we implement check_older in miniscript if we cannot use a Sequence number to satisfy the relative lock time?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I understand the question now. So, there are two pieces to locktime validation in Bitcoin:

  1. The CHECKSEQUENCEVERIFY (and CHECKLOCKTIMEVERIFY) opcodes, which check the sequence/locktime values in a transaction against some "test" sequence/locktime value, and decides whether or not it passes. This is what rust-miniscript uses when reasoning about check_older/check_after.
  2. Separately the Bitcoin blockchain checks whether the sequence/locktime values actually indicate a valid locktime, based on the state of the chain. This is what I think "normal users" are thinking of when they work with sequence/locktime values.

I would suggest that a "normal user" is more likely to be in situation (2), whereas situation (1) corresponds to Script interpretation. In this API we should somehow support both usecases somehow

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that my suggested API, in which is_satisfied_by takes both a height and time, would serve both cases.

In case (1), rust-miniscript will see check_older(N), decide whether to interpret N as a height or time, and then use is_satisfied_by_height/is_satisfied_by_time as appropriate. In case (2) the user has both the current height and current time, and can use is_satisfied_by.

Miniscript additionally needs a way to serialize a height/time as a "sequence number" to embed into Script, and to parse out such a value.

Copy link
Copy Markdown
Collaborator

@Kixunil Kixunil Aug 25, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idea how to resolve having to come up with different names:

// maybe this doesn't have to be sealed, IDK
pub trait SatisfiesRelativeLockTime: sealed::SatisfiesRelativeLockTime {
    fn satisfies_relative_lock_time(self, lock_time: relative::LockTime) -> bool;
}

impl SatisfiesRelativeLockTime for Sequence { /* ... */ }
impl SatisfiesRelativeLockTime for (Height, Time) { /* ... */ }
// impl SatisfiesRelativeLockTime for (Time, Height) { /* ... */ } // for convenience so people don't have to remember the order?

impl relative::LockTime {
    pub fn is_satisfied_by<T: SatisfiesRelativeLockTime>(self, satisified_by: T) -> bool {
        satisfied_by.satisfies_relative_lock_time(self)
    }
}

Note: not implementing SatisfiesRelativeLockTime for relative::LockTime is intentional because sequence may disable it entirely. Not implementing it prevents accidents.

Copy link
Copy Markdown
Member Author

@tcharding tcharding Aug 25, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the trait idea. Implementing for Sequence goes against what @apoelstra says above

I don't think it makes sense for a locktime to be satisfied by a sequence no, or vice-versa. Rather, I would expect a sequence number to be convertable to a relative::Locktime, and then this would be satisfied by a blockheight and/or timestamp.

^ quote by Andrew from above

Note: not implementing SatisfiesRelativeLockTime for relative::LockTime is intentional because sequence may disable it entirely. Not implementing it prevents accidents.

If Sequence disables lock time it is not possible to construct a relative::LockTime from the sequence so this concern is addressed. I think we should implement for relative::LockTime, (Height, Time), and (Time, Height).

Copy link
Copy Markdown
Member Author

@tcharding tcharding Aug 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I implemented for Height and Time as well as for the tuple of both and documented ... excessively.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If Sequence disables lock time it is not possible to construct a relative::LockTime from the sequence so this concern is addressed.

I still didn't like the possibility of manual check but now I think maybe that's too much paranoia.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, crap, I now realized it should've been the other way around. It makes sense to check if sequence is satisfied by (Height, Time)|RelativeLockTime but not that much for relative lock time.

@apoelstra
Copy link
Copy Markdown
Member

Done reviewing 2e6a8aefc048c28f71d8a4efb5f1d2766354c9f2.

@tcharding
Copy link
Copy Markdown
Member Author

I've updated rust-bitcoin/rust-miniscript#455 as well to demo current state of this PR.

@tcharding tcharding force-pushed the 08-16-relative-lock-time branch from 2e6a8ae to fb5abb4 Compare August 26, 2022 00:44
@tcharding
Copy link
Copy Markdown
Member Author

Implemented the LockTime::is_satisfied_by method using trait suggestion by @Kixunil. As a separate patch we remove the error used by Sequence and use the new one from the relative module (which I cut'n pasted). Can squash if requested.

@tcharding
Copy link
Copy Markdown
Member Author

Note that now in miniscript we have the super sexy

impl<Pk: MiniscriptKey + ToPublicKey> Satisfier<Pk> for Sequence {
    fn check_older(&self, n: relative::LockTime) -> bool {
        match self.to_relative_lock_time() {
            None => false,
            Some(lock_time) => n.is_satisfied_by(lock_time),
        }
    }
}

@tcharding tcharding added this to the 0.30.0 milestone Aug 26, 2022
@tcharding tcharding added enhancement minor API Change This PR should get a minor version bump release notes mention labels Aug 26, 2022
@Kixunil
Copy link
Copy Markdown
Collaborator

Kixunil commented Aug 26, 2022

Damn, now that I think about it, silently returning false in case of different units is probably a footgun. miniscript may need it and even use it correctly but it doesn't seem wise to provide such a footgun to everyone. We could return Result with Infallible in case of (Height, Time) but it's starting to get complicated.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fb5abb43711a9f39e62f0e7b25dda8f85b32e98b:

The masking seems unnecessary.

Copy link
Copy Markdown
Member Author

@tcharding tcharding Aug 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did it on purpose to make the as u16 explicitly safe because of the talk recently (@dpc and @Kixunil) about not using as. Can remove it (or leave it and document why).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a mild preference for removing it. I think the doccomment already justifies the loss of bits. The masking adds to the cognitive load.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure thing, thanks.

@apoelstra
Copy link
Copy Markdown
Member

Just a couple of nits.

I see what @Kixunil is saying that silently returning false (actually we return true now at my suggestion) when calling Height::satisfies_lock_time(time_based_lock_time) is possibly confusing. We could return an error but if satisfies_lock_time is a trait method then I really don't like this because then all the sane implementors will have to have an infallible error type which would suck with our MSRV.

I'm okay with the current API but my alternative would be for Height and Time to have their own, non-trait, satisfies_lock_time methods that return a Result and to leave the other 3 impls alone.

@tcharding
Copy link
Copy Markdown
Member Author

I added separate functions [back in] LockTime::is_satisfied_by_height and LockTime::is_satisfied_by_time that return a Result. Implemented them in bit of a roundabout way by calling Height::satisfies_lock_time which also returns a result (i.e., is a method not a trait method).

This results in a much cleaner API I found, as show by the examples in this version.

@tcharding tcharding force-pushed the 08-16-relative-lock-time branch 2 times, most recently from e729cd5 to 6a489a3 Compare August 30, 2022 03:54
@tcharding
Copy link
Copy Markdown
Member Author

Woops, I just squashed the transaction::RelativeLockTimeError -> relative::Error change into the last commit. I think we all know whats going on now, so leaving it as such. Holla if you want it split out.

@tcharding tcharding force-pushed the 08-16-relative-lock-time branch from 6a489a3 to b55fe3d Compare August 30, 2022 03:57
@tcharding
Copy link
Copy Markdown
Member Author

Rust question please: I've changed all the methods to take ownership of self. I believe we have discussed this before but I find myself again not knowing when to take ownership of self. I have been defaulting to only ever taking owneship if its needed in the method body. For Copy types should methods always take ownership?

@Kixunil
Copy link
Copy Markdown
Collaborator

Kixunil commented Aug 30, 2022

@tcharding Copy types are more convenient when taken by value. @dr-orlovsky had a good point they are convenient in combinators (iter.map(Type::some_fn)). Although maybe those returning bool are more convenient to take by reference because filter takes a reference (OTOH if you want to map iter to bools it sucks but hopefully less likely?)

@Kixunil
Copy link
Copy Markdown
Collaborator

Kixunil commented Aug 30, 2022

We could return an error but if satisfies_lock_time is a trait method then I really don't like this because then all the sane implementors will have to have an infallible error type which would suck with our MSRV.

The annoying part is calling .unwrap_or_else(|e| match e {}), indeed. An alternative suggestion:

pub trait SatisfiesRelativeLockTime: sealed::SatisfiesRelativeLockTime {
    type Error;

    fn satisfies_relative_lock_time(self, lock_time: relative::LockTime) -> Result<bool, Self::Error>;
}

// impl for `(Height, Time)`, `(Time, Height)` with `Error = Infallible`
// impl for `Height`, `Time, `Sequence` with `Error = UnitMismatchError`

impl relative::LockTime {
    pub fn is_satisfied_by<T: SatisfiesRelativeLockTime<Error=Infallible>>(self, satisified_by: T) -> bool {
        satisfied_by.satisfies_relative_lock_time(self).unwrap_or_else(|never| match never {})
    }

    pub fn try_is_satisfied_by<T: SatisfiesRelativeLockTime>(self, satisified_by: T) -> Result<bool, T::Error> {
        satisfied_by.satisfies_relative_lock_time(self)
    }
}

apoelstra
apoelstra previously approved these changes Aug 30, 2022
Copy link
Copy Markdown
Member

@apoelstra apoelstra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK b55fe3d89abb6b510c08aafed777af91a55111a9

@apoelstra
Copy link
Copy Markdown
Member

This looks good to me, though I'm not convinced that it's simpler than the equivalent code where we drop the traits and just (repetitively) inline all the impls, returning Result or not as appropriate.

@tcharding
Copy link
Copy Markdown
Member Author

Ha, after adding the result returning ones yesterday that thought did cross my mind. Glad you mentioned it. I removed the traits, simple code is goal. It was an educational exercise however.

We recently added the `Sequence` type and we explicitly did not include
relative lock time logic.

Add a new module `relative` and new type `LockTime` to represent
relative lock times as defined by BIP 68.
@tcharding tcharding force-pushed the 08-16-relative-lock-time branch from d9e3d0e to da95c3a Compare August 30, 2022 23:20
@apoelstra
Copy link
Copy Markdown
Member

I think this looks great! Maybe @Kixunil will disagree since we moved away from his suggested trait-based style.

Copy link
Copy Markdown
Member

@apoelstra apoelstra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK da95c3a

@tcharding
Copy link
Copy Markdown
Member Author

I think this looks great! Maybe @Kixunil will disagree since we moved away from his suggested trait-based style.

I don't think so since when that was suggested we didn't realize we wanted to return an error sometimes. @Kixunil, you cool with this as is?

Copy link
Copy Markdown
Member

@sanket1729 sanket1729 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK da95c3a. Left a minor nit

///
/// Will return an error if the input cannot be encoded in 16 bits.
#[inline]
pub fn from_seconds_ceil(seconds: u32) -> Result<Self, Error> {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Can this be an Option<T> instead? There is only one variant that can happen here. Most arithmetic operations in rust return an Option when the error is only possible before as overflow.
I would have suggested a dedicated error type, but it would be some work and a lot of code.

This would be annoying to unwrap because we not know all error variants

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm, a few considerations

  1. There is an equivalent method on Sequence so we should do the same for both
  2. It was a dedicated error type until I added the other variants
  3. Option has less information because the error returns the input value, is that useful? I don't know really (@Kixunil is the resident "useful error message" bloke, what do you think man?)

I can do either

  1. split the error into two different types
  2. Use an Option as suggested

I tend to agree that 2 is the best

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either way, I'll do it as a follow up please - this one has been running for long enough now.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally prefer custom error types to Option even at the cost of added code. It provides more information when people blindly ? it. (and .ok() is possible if they want to ignore)

A special type would be better indeed.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To expand on this a bit, in my experience sprinkling .ok_or{_else} or map_err everywhere is quite annoying and not doing it will bite you when function deep in the call stack returns error (None) and you don't even know which one it was. For instance this is very common when people ? std::io::Error.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sanket1729 could you raise an issue about this, if you feel strongly enough? I am going to merge this PR.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Furthering this discussion, I'm interested to learn if our method of having a single enum error type in each module (typically) is the correct way to go about things, alternatives (off the top of my head) are:

  • An individual error struct for every error
  • An enum for related errors
  • An enum error per "some other semantic block" e.g. all of the networking code

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My initial thoughts: for types convertible from byte arrays and such, parsing errors should be separate from other conversion errors - different layers-different errors
Functions that are very often called together can share error.
Everything else should have its own.

Splitting into crates will force us to at least split errors from independent parts.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't feel so strongly about it now. My workflow for detecting it is to try using it on a type that I know it should succeed on and match on error types before returning. I will raise issue/PR later about it

@Kixunil
Copy link
Copy Markdown
Collaborator

Kixunil commented Sep 1, 2022

I don't mind avoiding the trait, it was just an idea.

@apoelstra apoelstra merged commit 7efe30c into rust-bitcoin:master Sep 1, 2022
@tcharding tcharding deleted the 08-16-relative-lock-time branch September 2, 2022 02:19
@nlanson nlanson mentioned this pull request Sep 5, 2022
sander2 added a commit to sander2/polkabtc-clients that referenced this pull request Oct 6, 2022
sander2 added a commit to sander2/polkabtc-clients that referenced this pull request Oct 6, 2022
sander2 added a commit to sander2/polkabtc-clients that referenced this pull request Oct 7, 2022
moonman889 added a commit to moonman889/interbtc-clients that referenced this pull request Sep 15, 2025
neon-rider578 added a commit to neon-rider578/interbtc-clients that referenced this pull request Sep 30, 2025
stack-sage7291 added a commit to stack-sage7291/interbtc-clients that referenced this pull request Dec 11, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement minor API Change This PR should get a minor version bump release notes mention

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants