Skip to content

Commit 0ac479f

Browse files
committed
Merge #419: Fix bug in absolute timelock usage
42e1216 Unit test mixed up absolute timelocks (Tobin C. Harding) e4eb285 Add a minimal timelock module (Tobin C. Harding) 0b62f5e Fix incorrect timelock docs (Tobin C. Harding) Pull request description: Currently if we mix up height/time absolute timelocks when filtering policies the result is incorrect (to the best of my understanding). Add a `timelock` module that includes a single public function for checking if absolute timelocks are the same unit. Use the new function to fix a bug in policy filtering. - Patch 1 is a docs bug fix - Patch 2 is the bug fix - Patch 3 is a unit test patch that fails if its moved before patch 2. Please review the unit test carefully to make sure I'm not confused. ## Note There is [ongoing discussion](rust-bitcoin/rust-bitcoin#994) around trying to design a suitable timelock API. This PR is an attempt to make some forward progress by taking baby steps _and_ making objective improvements. I decided to do it here in miniscript because it will be easier to iterate on and more obvious when there are concrete usage examples along with each change. cc dpc ACKs for top commit: apoelstra: ACK 42e1216 Tree-SHA512: 93a55c550a903477cf20f50873d7b778bc36c1eaaf9a31c247d46d931f46100296d3fc3c89cf34ad6b029a7a14f6ae20d8208f22f4536fffe23cd5dab5fa674b
2 parents 56207d5 + 42e1216 commit 0ac479f

File tree

4 files changed

+49
-4
lines changed

4 files changed

+49
-4
lines changed

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ pub mod interpreter;
104104
pub mod miniscript;
105105
pub mod policy;
106106
pub mod psbt;
107+
pub mod timelock;
107108

108109
mod util;
109110

src/miniscript/types/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,13 +298,13 @@ pub trait Property: Sized {
298298
/// Type property of a timelock
299299
fn from_time(t: u32) -> Self;
300300

301-
/// Type property of a relative timelock. Default implementation simply
301+
/// Type property of an absolute timelock. Default implementation simply
302302
/// passes through to `from_time`
303303
fn from_after(t: u32) -> Self {
304304
Self::from_time(t)
305305
}
306306

307-
/// Type property of an absolute timelock. Default implementation simply
307+
/// Type property of a relative timelock. Default implementation simply
308308
/// passes through to `from_time`
309309
fn from_older(t: u32) -> Self {
310310
Self::from_time(t)

src/policy/semantic.rs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use bitcoin::hashes::{hash160, ripemd160, sha256, sha256d};
2222

2323
use super::concrete::PolicyError;
2424
use super::ENTAILMENT_MAX_TERMINALS;
25-
use crate::{errstr, expression, Error, ForEach, ForEachKey, MiniscriptKey};
25+
use crate::{errstr, expression, timelock, Error, ForEach, ForEachKey, MiniscriptKey};
2626

2727
/// Abstract policy which corresponds to the semantics of a Miniscript
2828
/// and which allows complex forms of analysis, e.g. filtering and
@@ -543,7 +543,9 @@ impl<Pk: MiniscriptKey> Policy<Pk> {
543543
pub fn at_height(mut self, time: u32) -> Policy<Pk> {
544544
self = match self {
545545
Policy::After(t) => {
546-
if t > time {
546+
if !timelock::absolute_timelocks_are_same_unit(t, time) {
547+
Policy::Unsatisfiable
548+
} else if t > time {
547549
Policy::Unsatisfiable
548550
} else {
549551
Policy::After(t)
@@ -762,6 +764,7 @@ mod tests {
762764
assert_eq!(policy.n_keys(), 0);
763765
assert_eq!(policy.minimum_n_keys(), Some(0));
764766

767+
// Block height 1000.
765768
let policy = StringPolicy::from_str("after(1000)").unwrap();
766769
assert_eq!(policy, Policy::After(1000));
767770
assert_eq!(policy.absolute_timelocks(), vec![1000]);
@@ -770,6 +773,26 @@ mod tests {
770773
assert_eq!(policy.clone().at_height(999), Policy::Unsatisfiable);
771774
assert_eq!(policy.clone().at_height(1000), policy.clone());
772775
assert_eq!(policy.clone().at_height(10000), policy.clone());
776+
// Pass a UNIX timestamp to at_height while policy uses a block height.
777+
assert_eq!(policy.clone().at_height(500_000_001), Policy::Unsatisfiable);
778+
assert_eq!(policy.n_keys(), 0);
779+
assert_eq!(policy.minimum_n_keys(), Some(0));
780+
781+
// UNIX timestamp of 10 seconds after the epoch.
782+
let policy = StringPolicy::from_str("after(500000010)").unwrap();
783+
assert_eq!(policy, Policy::After(500_000_010));
784+
assert_eq!(policy.absolute_timelocks(), vec![500_000_010]);
785+
assert_eq!(policy.relative_timelocks(), vec![]);
786+
// Pass a block height to at_height while policy uses a UNIX timestapm.
787+
assert_eq!(policy.clone().at_height(0), Policy::Unsatisfiable);
788+
assert_eq!(policy.clone().at_height(999), Policy::Unsatisfiable);
789+
assert_eq!(policy.clone().at_height(1000), Policy::Unsatisfiable);
790+
assert_eq!(policy.clone().at_height(10000), Policy::Unsatisfiable);
791+
// And now pass a UNIX timestamp to at_height while policy also uses a timestamp.
792+
assert_eq!(policy.clone().at_height(500_000_000), Policy::Unsatisfiable);
793+
assert_eq!(policy.clone().at_height(500_000_001), Policy::Unsatisfiable);
794+
assert_eq!(policy.clone().at_height(500_000_010), policy.clone());
795+
assert_eq!(policy.clone().at_height(500_000_012), policy.clone());
773796
assert_eq!(policy.n_keys(), 0);
774797
assert_eq!(policy.minimum_n_keys(), Some(0));
775798
}

src/timelock.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//! Various functions for manipulating Bitcoin timelocks.
2+
3+
use crate::miniscript::limits::LOCKTIME_THRESHOLD;
4+
5+
/// Returns true if `a` and `b` are the same unit i.e., both are block heights or both are UNIX
6+
/// timestamps. `a` and `b` are nLockTime values.
7+
pub fn absolute_timelocks_are_same_unit(a: u32, b: u32) -> bool {
8+
n_lock_time_is_block_height(a) == n_lock_time_is_block_height(b)
9+
}
10+
11+
// https://github.com/bitcoin/bitcoin/blob/9ccaee1d5e2e4b79b0a7c29aadb41b97e4741332/src/script/script.h#L39
12+
13+
/// Returns true if nLockTime `n` is to be interpreted as a block height.
14+
pub fn n_lock_time_is_block_height(n: u32) -> bool {
15+
n < LOCKTIME_THRESHOLD
16+
}
17+
18+
/// Returns true if nLockTime `n` is to be interpreted as a UNIX timestamp.
19+
pub fn n_lock_time_is_timestamp(n: u32) -> bool {
20+
n >= LOCKTIME_THRESHOLD
21+
}

0 commit comments

Comments
 (0)