Skip to content

Commit 668a2ae

Browse files
committed
descriptor: fix up regression_895 test
This is supposed to be testing mixed timelocks, but it uses descriptors that are invalid for several unrelated reasons. These will be enforced in a later PR, so fix the issues now. Specifically: * It is not allowed to use the same `unavailable_key` multiple times * The rightmost branch must not be sigless; the timelock needs to have a pubkey and'ed with it. Otherwise the whole script is malleable. * Similarly the "cheap dissatisfaction" of v:after(144),pk({})) must not be sigless. We turn it into v:after(144),and_v(v:pk({})),pk({})) which requires a signature even to be dissatisfied. * Also, the giant comments don't accurately reflect what's going on in this test. They claim that post-#895, we will refuse to create a satisfaction if we'd be forced into mixing timelocks. But we don't. We actually do the same wrong thing as before, just for different reasons. The resulting test now passes even when all `ValidationParams` are turned on, though in this PR we don't confirm this. HOWEVER, despite the test working with a non-malleable descriptor now, the satisfaction that this test produces is malleable. For this I have filed #976, which will be fixed in the next commit.
1 parent bdf50e3 commit 668a2ae

1 file changed

Lines changed: 89 additions & 41 deletions

File tree

src/miniscript/satisfy/mod.rs

Lines changed: 89 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -563,59 +563,103 @@ mod tests {
563563

564564
#[test]
565565
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.
566+
// Tests a pathological descriptor whose cheapest satisfaction involves computing a
567+
// dissatisfaction containing a timelock. Such dissatisfactions exist because the
568+
// `and_v` fragment, uniquely, has a dissatisfaction that includes the satisfaction
569+
// of its left child. (Check sat_dissat.rs and search 'dissat: ' to see how the
570+
// dissatisfactions of every fragment are computed. You will see that exactly one,
571+
// and_v, uses the satisfaction of a child.)
570572
//
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.
573+
// Prior to PR #895, the satisfier did not keep track of timelocks that were used
574+
// for dissatisfactions. This can lead to incorrect plans, as demonstrated in the
575+
// below test vectors.
579576

580-
// Setup: an unavailable key, used to make some branches unsatisfiable from the POV
577+
// Setup: unavailable keys, used to make some branches unsatisfiable from the POV
581578
// of the satisfier (but not the typechecker).
582-
let unavailable_key = PublicKey::from_str(
583-
"02eb64639a17f7334bb5a1a3aad857d6fec65faef439db3de72f85c88bc2906ad3",
584-
)
585-
.unwrap();
579+
let available_keys: [PublicKey; _] = [
580+
"02eb64639a17f7334bb5a1a3aad857d6fec65faef439db3de72f85c88bc2906ad1"
581+
.parse()
582+
.unwrap(),
583+
"02eb64639a17f7334bb5a1a3aad857d6fec65faef439db3de72f85c88bc2906ad3"
584+
.parse()
585+
.unwrap(),
586+
];
587+
let available_key_map = {
588+
let dummy_sig = bitcoin::ecdsa::Signature::sighash_all(
589+
secp256k1::ecdsa::Signature::from_compact(&[1; 64]).unwrap(),
590+
);
591+
let mut map = std::collections::BTreeMap::new();
592+
map.insert(
593+
crate::DefiniteDescriptorKey::new(available_keys[0].into()).unwrap(),
594+
dummy_sig,
595+
);
596+
map.insert(
597+
crate::DefiniteDescriptorKey::new(available_keys[1].into()).unwrap(),
598+
dummy_sig,
599+
);
600+
map
601+
};
602+
603+
// Generate a large pile of distinct unavailable keys.
604+
let secp = secp256k1::Secp256k1::new();
605+
let unavailable_keys: Vec<PublicKey> =
606+
core::iter::successors(Some(available_keys[0]), |prev| {
607+
prev.inner
608+
.add_exp_tweak(&secp, &secp256k1::Scalar::ONE)
609+
.ok()
610+
.map(PublicKey::new)
611+
})
612+
.skip(1)
613+
.take(20)
614+
.collect();
615+
assert_eq!(unavailable_keys.len(), 20); // sanity-check the above loop
616+
617+
// Create a fragment which is very expensive to dissatisfy.
618+
let expensive_threshold = format!(
619+
"thresh(10,pk({}),{})",
620+
unavailable_keys[1], // we need key[0] below
621+
unavailable_keys
622+
.iter()
623+
.skip(2)
624+
.map(|k| format!("a:pkh({k})"))
625+
.collect::<Vec<_>>()
626+
.join(","),
627+
);
628+
586629
// Setup: a satisfier that thinks that both a height-based and time-based timelock
587630
// are available. This is needed for the second test.
588631
let satisfier = (
632+
available_key_map,
589633
absolute::LockTime::from_height(1000).unwrap(),
590634
absolute::LockTime::from_time(2000000000).unwrap(),
591635
);
592636

593637
// Construct a script that would mix timelocks:
594638
// or_b(
595639
// 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)
640+
// and_v(v:after(144),and_v(v:pk(),pk())), // dissatisfied by a height-based timelock, a sig, and 0
641+
// expensive_threshold // dissatisfied by a giant pile of pubkeyhash preimages
598642
// ),
599-
// sdv:after(50) // satisfied by a lower height-based timelock
643+
// ajt:and_v(v:after(50),v:pk({})))) // satisfied by a lower height-based timelock and a sig
600644
// )
601645
//
602-
// Here the or_i cannot be satisfied because none of the keys are available, so it
646+
// Here the or_i cannot be satisfied due to missing keys on both branches, so it
603647
// must be dissatisfied (and the after(50) branch must be satisfied). However, there
604648
// are two dissatisfactions for the or_i: one which dissatisfies the first branch,
605649
// by using the height-based timelock, and one which dissatisfies the second branch,
606650
// which is ignored since it's the more expensive of the two possibilities.
607651
//
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.
652+
// Since the first branch's dissatisfaction is HASSIG but the second branch's is not,
653+
// the satisfier is required to dissatisfy the second branch to avoid malleability.
654+
// However, because of #976, it does not.
611655
//
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-
656+
// Itstead, it takes both the after(144) and after(50) branches, and the resulting
657+
// plan should show after(144) since it's the higher one. However, prior to #895,
658+
// we "did not notice" the after(144) since it appears as part of a dissatisfaction,
659+
// leading to a plan that did not match the actual timelock requirement.
616660
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,
661+
"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({}))))",
662+
available_keys[0], unavailable_keys[0], available_keys[1],
619663
);
620664
// Need DefiniteDescriptorKey https://github.com/rust-bitcoin/rust-miniscript/issues/927
621665
let descriptor =
@@ -625,19 +669,23 @@ mod tests {
625669
assert_eq!(plan.absolute_timelock, Some(absolute::LockTime::from_height(144).unwrap()),);
626670

627671
// 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.)
672+
// lower height-based one.
673+
//
674+
// Again, we wind up taking both timelock branches. Prior to #895, we would do this because
675+
// we did not track the after(144) at all. After #895, we do it because our timelock logic
676+
// handles mixed timelocks by silently clobbering one of them. (See the implementation of
677+
// `concatenate_rev`, which calls `cmp::max` on the timelocks. This clobbers a locktime.
678+
// See #979 for an explanation of this.)
679+
//
680+
// This time the "ideal" behavior would be that we track best satisfactions for both time-
681+
// and height-based locktimes, and whenever we are forced into a conflict we throw one away.
682+
// (The type system guarantees that we will always have at least one satisfaction left;
683+
// otherwise the whole script would be flagged as mixing timelocks.)
632684
//
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.
685+
// For now we just use this unit test to document the behavior.
638686
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,
687+
"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({}))))",
688+
available_keys[0], unavailable_keys[0], available_keys[1],
641689
);
642690
let descriptor =
643691
Descriptor::<crate::DefiniteDescriptorKey>::from_str(&descriptor_str).unwrap();

0 commit comments

Comments
 (0)