@@ -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
@@ -563,89 +565,138 @@ mod tests {
563565
564566 #[ test]
565567 fn regression_895 ( ) {
566- // Tests a pathological descriptor whose cheapest satisfaction would require mixing
567- // timelocks, although a more expensive satisfaction is available that avoids the
568- // timelock mixing. This descriptor is accepted by rust-miniscript but its satisfier
569- // cannot produce a satisfaction for it.
568+ // Tests a pathological descriptor whose cheapest satisfaction involves computing a
569+ // dissatisfaction containing a timelock. Such dissatisfactions exist because the
570+ // `and_v` fragment, uniquely, has a dissatisfaction that includes the satisfaction
571+ // of its left child. (Check sat_dissat.rs and search 'dissat: ' to see how the
572+ // dissatisfactions of every fragment are computed. You will see that exactly one,
573+ // and_v, uses the satisfaction of a child.)
570574 //
571- // Prior to PR #895, the satisfaction logic would yield the invalid timelock-mixing
572- // satisfaction. After PR #895, it yields no satisfaction at all.
573- //
574- // The correct behavior is arguably to find the more expensive satisfaction. Doing
575- // this would would require some sort of backtracking in the satisfier and is highly
576- // nontrivial. Since triggering this bug requires generating a satisfaction in a
577- // context where both a height-based and time-based timelock are available, an
578- // impossible situation, it is probably not worth fixing.
575+ // Prior to PR #895, the satisfier did not keep track of timelocks that were used
576+ // for dissatisfactions. This can lead to incorrect plans, as demonstrated in the
577+ // below test vectors.
579578
580- // Setup: an unavailable key , used to make some branches unsatisfiable from the POV
579+ // Setup: unavailable keys , used to make some branches unsatisfiable from the POV
581580 // of the satisfier (but not the typechecker).
582- let unavailable_key = PublicKey :: from_str (
583- "02eb64639a17f7334bb5a1a3aad857d6fec65faef439db3de72f85c88bc2906ad3" ,
584- )
585- . unwrap ( ) ;
581+ let available_keys: [ PublicKey ; 2 ] = [
582+ "02eb64639a17f7334bb5a1a3aad857d6fec65faef439db3de72f85c88bc2906ad1"
583+ . parse ( )
584+ . unwrap ( ) ,
585+ "02eb64639a17f7334bb5a1a3aad857d6fec65faef439db3de72f85c88bc2906ad3"
586+ . parse ( )
587+ . unwrap ( ) ,
588+ ] ;
589+ let available_key_map = {
590+ let dummy_sig = bitcoin:: ecdsa:: Signature :: sighash_all (
591+ secp256k1:: ecdsa:: Signature :: from_compact ( & [ 1 ; 64 ] ) . unwrap ( ) ,
592+ ) ;
593+ let mut map = std:: collections:: BTreeMap :: new ( ) ;
594+ map. insert (
595+ crate :: DefiniteDescriptorKey :: new ( available_keys[ 0 ] . into ( ) ) . unwrap ( ) ,
596+ dummy_sig,
597+ ) ;
598+ map. insert (
599+ crate :: DefiniteDescriptorKey :: new ( available_keys[ 1 ] . into ( ) ) . unwrap ( ) ,
600+ dummy_sig,
601+ ) ;
602+ map
603+ } ;
604+
605+ // Generate a large pile of distinct unavailable keys.
606+ let secp = secp256k1:: Secp256k1 :: new ( ) ;
607+ let unavailable_keys: Vec < PublicKey > =
608+ core:: iter:: successors ( Some ( available_keys[ 0 ] ) , |prev| {
609+ prev. inner
610+ . add_exp_tweak ( & secp, & secp256k1:: Scalar :: ONE )
611+ . ok ( )
612+ . map ( PublicKey :: new)
613+ } )
614+ . skip ( 1 )
615+ . take ( 20 )
616+ . collect ( ) ;
617+ assert_eq ! ( unavailable_keys. len( ) , 20 ) ; // sanity-check the above loop
618+
619+ // Create a fragment which is very expensive to dissatisfy.
620+ let expensive_threshold = format ! (
621+ "thresh(10,pk({}),{})" ,
622+ unavailable_keys[ 1 ] , // we need key[0] below
623+ unavailable_keys
624+ . iter( )
625+ . skip( 2 )
626+ . map( |k| format!( "a:pkh({k})" ) )
627+ . collect:: <Vec <_>>( )
628+ . join( "," ) ,
629+ ) ;
630+
586631 // Setup: a satisfier that thinks that both a height-based and time-based timelock
587632 // are available. This is needed for the second test.
588633 let satisfier = (
634+ available_key_map,
589635 absolute:: LockTime :: from_height ( 1000 ) . unwrap ( ) ,
590636 absolute:: LockTime :: from_time ( 2000000000 ) . unwrap ( ) ,
591637 ) ;
592638
593639 // Construct a script that would mix timelocks:
594640 // or_b(
595641 // n:or_i(
596- // and_v(v:after(144),pk()), // dissatisfied by a height-based timelock
597- // thresh(3,pk(),s:pk(),s:pk()) // dissatisfied by 3empty sigs (more expensive)
642+ // and_v(v:after(144),and_v(v: pk(),pk())), // dissatisfied by a height-based timelock, a sig, and 0
643+ // expensive_threshold // dissatisfied by a giant pile of pubkeyhash preimages
598644 // ),
599- // sdv: after(50) // satisfied by a lower height-based timelock
645+ // ajt:and_v(v: after(50),v:pk({})))) // satisfied by a lower height-based timelock and a sig
600646 // )
601647 //
602- // Here the or_i cannot be satisfied because none of the keys are available , so it
648+ // Here the or_i cannot be satisfied due to missing keys on both branches , so it
603649 // must be dissatisfied (and the after(50) branch must be satisfied). However, there
604650 // are two dissatisfactions for the or_i: one which dissatisfies the first branch,
605651 // by using the height-based timelock, and one which dissatisfies the second branch,
606652 // which is ignored since it's the more expensive of the two possibilities.
607653 //
608- // We therefore take both the after(144) and after(50) branches, and the resulting
609- // plan should show after(144) since it's the higher one. However, prior to #895,
610- // we "did not notice" the after(144) since it appears as part of a dissatisfaction .
654+ // Since the first branch's dissatisfaction is HASSIG but the second branch's is not,
655+ // the satisfier is required to dissatisfy the second branch to avoid malleability.
656+ // But if we use `into_plan_mall` we can see the bug .
611657 //
612- // Unrelatedly: the fact that the LHS of the `or_i` has a malleable dissatisfaction
613- // means that the whole script is malleable, and the fact that this parses at all is
614- // an instance of https://github.com/rust-bitcoin/rust-miniscript/issues/734
615-
658+ // Itstead, it takes both the after(144) and after(50) branches, and the resulting
659+ // plan should show after(144) since it's the higher one. However, prior to #895,
660+ // we "did not notice" the after(144) since it appears as part of a dissatisfaction,
661+ // leading to a plan that did not match the actual timelock requirement.
616662 let descriptor_str = format ! (
617- "wsh(or_b(n:or_i(and_v(v:after(144),pk({})),thresh(3, pk({}),s:pk({ }),s:pk({}))),sdv :after(50)))" ,
618- unavailable_key , unavailable_key , unavailable_key , unavailable_key ,
663+ "wsh(or_b(n:or_i(and_v(v:after(144),and_v(v: pk({}), pk({}))),{expensive_threshold }),ajt:and_v(v :after(50),v:pk({}) )))" ,
664+ available_keys [ 0 ] , unavailable_keys [ 0 ] , available_keys [ 1 ] ,
619665 ) ;
620666 // Need DefiniteDescriptorKey https://github.com/rust-bitcoin/rust-miniscript/issues/927
621667 let descriptor =
622668 Descriptor :: < crate :: DefiniteDescriptorKey > :: from_str ( & descriptor_str) . unwrap ( ) ;
623- // Compute plan and confirm the timelock is correct.
624- let plan = descriptor. into_plan ( & satisfier) . unwrap ( ) ;
669+ // Compute plan and confirm the timelock is correct -- 144 for a malleable transaction
670+ let plan = descriptor. clone ( ) . into_plan_mall ( & satisfier) . unwrap ( ) ;
625671 assert_eq ! ( plan. absolute_timelock, Some ( absolute:: LockTime :: from_height( 144 ) . unwrap( ) ) , ) ;
672+ // ...and 50 for a non-malleable one (since take the expensive_threshold alternate)
673+ let plan = descriptor. into_plan ( & satisfier) . unwrap ( ) ;
674+ assert_eq ! ( plan. absolute_timelock, Some ( absolute:: LockTime :: from_height( 50 ) . unwrap( ) ) , ) ;
626675
627676 // Same descriptor as above, except that now we use a time-based timelock rather than a
628- // lower height-based one. This time the "ideal" behavior would be that once we get to
629- // the final time-based timelock, we somehow backtrack and then use the more-expensive
630- // dissatisfaction choice for the `or_i`. (The type system guarantees that such a choice
631- // exists; otherwise the whole script would be flagged as mixing timelocks.)
677+ // lower height-based one.
632678 //
633- // However, our code architecture doesn't let us backtrack like this, and it's only possible
634- // to get into this situation if (a) you have a pathological script like this, and (b) you
635- // call .plan() or .satisfy() with both a height-based and time-based timelock set (which
636- // is impossible for any actual transaction). So for now we just use this unit test to
637- // 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).
638683 let descriptor_str = format ! (
639- "wsh(or_b(n:or_i(and_v(v:after(144),pk({})),thresh(2, pk({}),s:pk({}))),sdv: after(1000000000)))" ,
640- unavailable_key , unavailable_key , unavailable_key ,
684+ "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({}) )))" ,
685+ available_keys [ 0 ] , unavailable_keys [ 0 ] , available_keys [ 1 ] ,
641686 ) ;
642687 let descriptor =
643688 Descriptor :: < crate :: DefiniteDescriptorKey > :: from_str ( & descriptor_str) . unwrap ( ) ;
644689
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.
645696 let plan = descriptor. into_plan ( & satisfier) . unwrap ( ) ;
646697 assert_eq ! (
647698 plan. absolute_timelock,
648- Some ( absolute:: LockTime :: from_time( 1000000000 ) . unwrap( ) ) ,
699+ Some ( absolute:: LockTime :: from_time( 1_000_000_000 ) . unwrap( ) ) ,
649700 ) ;
650701 }
651702}
@@ -978,6 +1029,13 @@ pub struct Satisfaction<T> {
9781029}
9791030
9801031impl < 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+
9811039 /// The empty satisfaction.
9821040 ///
9831041 /// This has the property that, when concatenated on either side with another satisfaction
@@ -997,11 +1055,36 @@ impl<Pk: MiniscriptKey + ToPublicKey> Satisfaction<Placeholder<Pk>> {
9971055 /// This order allows callers to write `left.concatenate_rev(right)` which feels more
9981056 /// natural than the opposite order, and more importantly, allows this method to be
9991057 /// 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.
10001064 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+
10011084 Self {
10021085 has_sig : self . has_sig || other. has_sig ,
1003- relative_timelock : cmp :: max ( self . relative_timelock , other . relative_timelock ) ,
1004- absolute_timelock : cmp :: max ( self . absolute_timelock , other . absolute_timelock ) ,
1086+ relative_timelock,
1087+ absolute_timelock,
10051088 stack : Witness :: combine ( other. stack , self . stack ) ,
10061089 }
10071090 }
@@ -1069,14 +1152,7 @@ impl<Pk: MiniscriptKey + ToPublicKey> Satisfaction<Placeholder<Pk>> {
10691152 // For example, the fragment thresh(2, hash, 0, 0, 0)
10701153 // is has an impossible witness
10711154 if sats[ sat_indices[ k - 1 ] ] . stack == Witness :: Impossible {
1072- Self {
1073- stack : Witness :: Impossible ,
1074- // If the witness is impossible, we don't care about the
1075- // has_sig flag, nor about the timelocks
1076- has_sig : false ,
1077- relative_timelock : None ,
1078- absolute_timelock : None ,
1079- }
1155+ Self :: IMPOSSIBLE
10801156 }
10811157 // We are now guaranteed that all elements in `k` satisfactions
10821158 // are not impossible(we sort by is_impossible bool).
0 commit comments