@@ -93,18 +93,20 @@ pub trait Satisfier<Pk: MiniscriptKey + ToPublicKey> {
9393 /// Given a HASH160 hash, look up its preimage
9494 fn lookup_hash160 ( & self , _: & Pk :: Hash160 ) -> Option < Preimage32 > { None }
9595
96- /// Assert whether an relative locktime is satisfied
96+ /// Returns whether the given relative locktime is satisfied.
9797 ///
98- /// NOTE: If a descriptor mixes time-based and height-based timelocks, the implementation of
99- /// this method MUST only allow timelocks of either unit, but not both. Allowing both could cause
100- /// miniscript to construct an invalid witness.
98+ /// Composite satisfiers may expose both height- and time-based locktimes.
99+ /// If a single witness path would require incompatible timelock kinds
100+ /// simultaneously, satisfaction for that path becomes [`Witness::Impossible`]
101+ /// and plan building fails or selects another path.
101102 fn check_older ( & self , _: relative:: LockTime ) -> bool { false }
102103
103- /// Assert whether a absolute locktime is satisfied
104+ /// Returns whether the given absolute locktime is satisfied.
104105 ///
105- /// NOTE: If a descriptor mixes time-based and height-based timelocks, the implementation of
106- /// this method MUST only allow timelocks of either unit, but not both. Allowing both could cause
107- /// miniscript to construct an invalid witness.
106+ /// Composite satisfiers may expose both height- and time-based locktimes.
107+ /// If a single witness path would require incompatible timelock kinds
108+ /// simultaneously, satisfaction for that path becomes [`Witness::Impossible`]
109+ /// and plan building fails or selects another path.
108110 fn check_after ( & self , _: absolute:: LockTime ) -> bool { false }
109111}
110112
@@ -674,29 +676,27 @@ mod tests {
674676 // Same descriptor as above, except that now we use a time-based timelock rather than a
675677 // lower height-based one.
676678 //
677- // Again, we wind up taking both timelock branches. Prior to #895, we would do this because
678- // we did not track the after(144) at all. After #895, we do it because our timelock logic
679- // handles mixed timelocks by silently clobbering one of them. (See the implementation of
680- // `concatenate_rev`, which calls `cmp::max` on the timelocks. This clobbers a locktime.
681- // See #979 for an explanation of this.)
682- //
683- // This time the "ideal" behavior would be that we track best satisfactions for both time-
684- // and height-based locktimes, and whenever we are forced into a conflict we throw one away.
685- // (The type system guarantees that we will always have at least one satisfaction left;
686- // otherwise the whole script would be flagged as mixing timelocks.)
687- //
688- // For now we just use this unit test to document the behavior.
679+ // Again, both timelock branches are considered. When `concatenate_rev` must merge a
680+ // height-based and a time-based absolute locktime on the same path, that satisfaction
681+ // becomes `Witness::Impossible`. Plan building then fails or selects an alternate path.
682+ // Parallel per-kind tracking remains future work (see #979 discussion).
689683 let descriptor_str = format ! (
690684 "wsh(or_b(n:or_i(and_v(v:after(144),and_v(v:pk({}),pk({}))),{expensive_threshold}),ajt:and_v(v:after(1000000000),v:pk({}))))" ,
691685 available_keys[ 0 ] , unavailable_keys[ 0 ] , available_keys[ 1 ] ,
692686 ) ;
693687 let descriptor =
694688 Descriptor :: < crate :: DefiniteDescriptorKey > :: from_str ( & descriptor_str) . unwrap ( ) ;
695689
696- let plan = descriptor. into_plan_mall ( & satisfier) . unwrap ( ) ;
690+ // Both `or_b` arms hit a height/time conflict in `concatenate_rev`, so no malleable
691+ // plan exists.
692+ assert ! ( descriptor. clone( ) . into_plan_mall( & satisfier) . is_err( ) ) ;
693+ // Non-malleable plan still succeeds: `into_plan` dissatisfies the expensive threshold
694+ // branch and satisfies the time-based `after(1000000000)` branch without merging
695+ // incompatible locktimes on one path.
696+ let plan = descriptor. into_plan ( & satisfier) . unwrap ( ) ;
697697 assert_eq ! (
698698 plan. absolute_timelock,
699- Some ( absolute:: LockTime :: from_time( 1000000000 ) . unwrap( ) ) ,
699+ Some ( absolute:: LockTime :: from_time( 1_000_000_000 ) . unwrap( ) ) ,
700700 ) ;
701701 }
702702}
@@ -1029,6 +1029,13 @@ pub struct Satisfaction<T> {
10291029}
10301030
10311031impl < Pk : MiniscriptKey + ToPublicKey > Satisfaction < Placeholder < Pk > > {
1032+ const IMPOSSIBLE : Self = Self {
1033+ stack : Witness :: Impossible ,
1034+ has_sig : false ,
1035+ relative_timelock : None ,
1036+ absolute_timelock : None ,
1037+ } ;
1038+
10321039 /// The empty satisfaction.
10331040 ///
10341041 /// This has the property that, when concatenated on either side with another satisfaction
@@ -1048,11 +1055,36 @@ impl<Pk: MiniscriptKey + ToPublicKey> Satisfaction<Placeholder<Pk>> {
10481055 /// This order allows callers to write `left.concatenate_rev(right)` which feels more
10491056 /// natural than the opposite order, and more importantly, allows this method to be
10501057 /// used when folding over an iterator of multiple satisfactions.
1058+ ///
1059+ /// Same-unit locktimes merge to the later value via [`AbsLockTime::max`] and
1060+ /// [`RelLockTime::max`]. Mixed height/time on the same path yields
1061+ /// [`Witness::Impossible`]. Downstream [`Self::minimum`], [`Self::minimum_mall`], and
1062+ /// [`Self::thresh`] treat Impossible like other dead branches; [`Descriptor::into_plan`] and
1063+ /// [`Descriptor::into_plan_mall`] fail if the winning path is Impossible.
10511064 fn concatenate_rev ( self , other : Self ) -> Self {
1065+ if self . stack == Witness :: Impossible || other. stack == Witness :: Impossible {
1066+ return Self :: IMPOSSIBLE ;
1067+ }
1068+
1069+ let relative_timelock = match ( self . relative_timelock , other. relative_timelock ) {
1070+ ( None , x) | ( x, None ) => x,
1071+ ( Some ( a) , Some ( b) ) => match RelLockTime :: max ( a, b) {
1072+ Some ( t) => Some ( t) ,
1073+ None => return Self :: IMPOSSIBLE ,
1074+ } ,
1075+ } ;
1076+ let absolute_timelock = match ( self . absolute_timelock , other. absolute_timelock ) {
1077+ ( None , x) | ( x, None ) => x,
1078+ ( Some ( a) , Some ( b) ) => match AbsLockTime :: max ( a, b) {
1079+ Some ( t) => Some ( t) ,
1080+ None => return Self :: IMPOSSIBLE ,
1081+ } ,
1082+ } ;
1083+
10521084 Self {
10531085 has_sig : self . has_sig || other. has_sig ,
1054- relative_timelock : cmp :: max ( self . relative_timelock , other . relative_timelock ) ,
1055- absolute_timelock : cmp :: max ( self . absolute_timelock , other . absolute_timelock ) ,
1086+ relative_timelock,
1087+ absolute_timelock,
10561088 stack : Witness :: combine ( other. stack , self . stack ) ,
10571089 }
10581090 }
@@ -1120,14 +1152,7 @@ impl<Pk: MiniscriptKey + ToPublicKey> Satisfaction<Placeholder<Pk>> {
11201152 // For example, the fragment thresh(2, hash, 0, 0, 0)
11211153 // is has an impossible witness
11221154 if sats[ sat_indices[ k - 1 ] ] . stack == Witness :: Impossible {
1123- Self {
1124- stack : Witness :: Impossible ,
1125- // If the witness is impossible, we don't care about the
1126- // has_sig flag, nor about the timelocks
1127- has_sig : false ,
1128- relative_timelock : None ,
1129- absolute_timelock : None ,
1130- }
1155+ Self :: IMPOSSIBLE
11311156 }
11321157 // We are now guaranteed that all elements in `k` satisfactions
11331158 // are not impossible(we sort by is_impossible bool).
0 commit comments