Skip to content

Commit 200991d

Browse files
committed
compiler: improve logic for deciding between thresholds and ands
In our threshold logic we have some special cases to handle multi, multi_a, and conjunctions. However, we were not choosing between these special cases correctly. Our logic was roughly that we would try to use multi_a if all children were pk()s, OR try to use multi if all children were pk() and there weren't too many, OR try to use a conjunction if k == n. The correct logic is: if all children are keys, and there aren't too many, try to use multi or multi_a. ALSO, if k == n, try to use a conjunction. With this fix, the compiler correctly finds that conjunctions are more efficient than CHECKMULTISIG when k == n and n < 3. When n == 3 the two cases have equal cost, but it seems to prefer the conjunction. It also correctly finds that when k == n, it is always more efficient to use a conjunction than to use CHECKSIGADD. This change necessitates changing some tests.
1 parent bc3b8dc commit 200991d

File tree

5 files changed

+59
-43
lines changed

5 files changed

+59
-43
lines changed

examples/taproot.rs

+6-6
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ fn main() {
4545
let desc = pol.compile_tr(Some("UNSPENDABLE_KEY".to_string())).unwrap();
4646

4747
let expected_desc =
48-
Descriptor::<String>::from_str("tr(Ca,{and_v(v:pk(In),older(9)),multi_a(2,hA,S)})")
48+
Descriptor::<String>::from_str("tr(Ca,{and_v(v:pk(In),older(9)),and_v(v:pk(hA),pk(S))})")
4949
.unwrap();
5050
assert_eq!(desc, expected_desc);
5151

@@ -73,7 +73,7 @@ fn main() {
7373
);
7474
assert_eq!(
7575
iter.next().unwrap(),
76-
(1u8, &Miniscript::<String, Tap>::from_str("multi_a(2,hA,S)").unwrap())
76+
(1u8, &Miniscript::<String, Tap>::from_str("and_v(v:pk(hA),pk(S))").unwrap())
7777
);
7878
assert_eq!(iter.next(), None);
7979
}
@@ -97,19 +97,19 @@ fn main() {
9797
let real_desc = desc.translate_pk(&mut t).unwrap();
9898

9999
// Max satisfaction weight for compilation, corresponding to the script-path spend
100-
// `multi_a(2,PUBKEY_1,PUBKEY_2) at tap tree depth 1, having:
100+
// `and_v(PUBKEY_1,PUBKEY_2) at tap tree depth 1, having:
101101
//
102102
// max_witness_size = varint(control_block_size) + control_block size +
103103
// varint(script_size) + script_size + max_satisfaction_size
104-
// = 1 + 65 + 1 + 70 + 132 = 269
104+
// = 1 + 65 + 1 + 68 + 132 = 269
105105
let max_sat_wt = real_desc.max_weight_to_satisfy().unwrap();
106-
assert_eq!(max_sat_wt, 269);
106+
assert_eq!(max_sat_wt, 267);
107107

108108
// Compute the bitcoin address and check if it matches
109109
let network = Network::Bitcoin;
110110
let addr = real_desc.address(network).unwrap();
111111
let expected_addr = bitcoin::Address::from_str(
112-
"bc1pcc8ku64slu3wu04a6g376d2s8ck9y5alw5sus4zddvn8xgpdqw2swrghwx",
112+
"bc1p4l2xzq7js40965s5w0fknd287kdlmt2dljte37zsc5a34u0h9c4q85snyd",
113113
)
114114
.unwrap()
115115
.assume_checked();

src/miniscript/decode.rs

+3-5
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,10 @@ use core::marker::PhantomData;
1111
use std::error;
1212

1313
use bitcoin::hashes::{hash160, ripemd160, sha256, Hash};
14-
use bitcoin::Weight;
1514
use sync::Arc;
1615

1716
use crate::miniscript::lex::{Token as Tk, TokenIter};
18-
use crate::miniscript::limits::MAX_PUBKEYS_PER_MULTISIG;
17+
use crate::miniscript::limits::{MAX_PUBKEYS_IN_CHECKSIGADD, MAX_PUBKEYS_PER_MULTISIG};
1918
use crate::miniscript::types::extra_props::ExtData;
2019
use crate::miniscript::types::Type;
2120
use crate::miniscript::ScriptContext;
@@ -451,10 +450,9 @@ pub fn parse<Ctx: ScriptContext>(
451450
},
452451
// MultiA
453452
Tk::NumEqual, Tk::Num(k) => {
454-
let max = Weight::MAX_BLOCK.to_wu() / 32;
455453
// Check size before allocating keys
456-
if k as u64 > max {
457-
return Err(Error::MultiATooManyKeys(max))
454+
if k as usize > MAX_PUBKEYS_IN_CHECKSIGADD {
455+
return Err(Error::MultiATooManyKeys(MAX_PUBKEYS_IN_CHECKSIGADD as u64))
458456
}
459457
let mut keys = Vec::with_capacity(k as usize); // atleast k capacity
460458
while tokens.peek() == Some(&Tk::CheckSigAdd) {

src/miniscript/limits.rs

+2
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,5 @@ pub const MAX_BLOCK_WEIGHT: usize = 4000000;
3333
/// Maximum pubkeys as arguments to CHECKMULTISIG
3434
// https://github.com/bitcoin/bitcoin/blob/6acda4b00b3fc1bfac02f5de590e1a5386cbc779/src/script/script.h#L30
3535
pub const MAX_PUBKEYS_PER_MULTISIG: usize = 20;
36+
/// Maximum pubkeys in a CHECKSIGADD construction.
37+
pub const MAX_PUBKEYS_IN_CHECKSIGADD: usize = (bitcoin::Weight::MAX_BLOCK.to_wu() / 32) as usize;

src/policy/compiler.rs

+38-23
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use std::error;
1212
use sync::Arc;
1313

1414
use crate::miniscript::context::SigType;
15-
use crate::miniscript::limits::MAX_PUBKEYS_PER_MULTISIG;
15+
use crate::miniscript::limits::{MAX_PUBKEYS_IN_CHECKSIGADD, MAX_PUBKEYS_PER_MULTISIG};
1616
use crate::miniscript::types::{self, ErrorKind, ExtData, Type};
1717
use crate::miniscript::ScriptContext;
1818
use crate::policy::Concrete;
@@ -1065,24 +1065,26 @@ where
10651065
})
10661066
.collect();
10671067

1068-
match Ctx::sig_type() {
1069-
SigType::Schnorr if key_vec.len() == subs.len() => {
1070-
insert_wrap!(AstElemExt::terminal(Terminal::MultiA(k, key_vec)))
1071-
}
1072-
SigType::Ecdsa
1073-
if key_vec.len() == subs.len() && subs.len() <= MAX_PUBKEYS_PER_MULTISIG =>
1074-
{
1075-
insert_wrap!(AstElemExt::terminal(Terminal::Multi(k, key_vec)))
1068+
if key_vec.len() == subs.len() {
1069+
match Ctx::sig_type() {
1070+
SigType::Schnorr => {
1071+
if key_vec.len() <= MAX_PUBKEYS_IN_CHECKSIGADD {
1072+
insert_wrap!(AstElemExt::terminal(Terminal::MultiA(k, key_vec)))
1073+
}
1074+
}
1075+
SigType::Ecdsa => {
1076+
if key_vec.len() <= MAX_PUBKEYS_PER_MULTISIG {
1077+
insert_wrap!(AstElemExt::terminal(Terminal::Multi(k, key_vec)))
1078+
}
1079+
}
10761080
}
1077-
_ if k == subs.len() => {
1078-
let mut it = subs.iter();
1079-
let mut policy = it.next().expect("No sub policy in thresh() ?").clone();
1080-
policy =
1081-
it.fold(policy, |acc, pol| Concrete::And(vec![acc, pol.clone()]).into());
1081+
}
1082+
if k == subs.len() {
1083+
let mut it = subs.iter();
1084+
let mut policy = it.next().expect("No sub policy in thresh() ?").clone();
1085+
policy = it.fold(policy, |acc, pol| Concrete::And(vec![acc, pol.clone()]).into());
10821086

1083-
ret = best_compilations(policy_cache, policy.as_ref(), sat_prob, dissat_prob)?;
1084-
}
1085-
_ => {}
1087+
ret = best_compilations(policy_cache, policy.as_ref(), sat_prob, dissat_prob)?;
10861088
}
10871089

10881090
// FIXME: Should we also optimize thresh(1, subs) ?
@@ -1501,13 +1503,19 @@ mod tests {
15011503
fn compile_thresh() {
15021504
let (keys, _) = pubkeys_and_a_sig(21);
15031505

1504-
// Up until 20 keys, thresh should be compiled to a multi no matter the value of k
1506+
// For 3 < n <= 20, thresh should be compiled to a multi no matter the value of k
15051507
for k in 1..4 {
1506-
let small_thresh: BPolicy =
1507-
policy_str!("thresh({},pk({}),pk({}),pk({}))", k, keys[0], keys[1], keys[2]);
1508+
let small_thresh: BPolicy = policy_str!(
1509+
"thresh({},pk({}),pk({}),pk({}),pk({}))",
1510+
k,
1511+
keys[0],
1512+
keys[1],
1513+
keys[2],
1514+
keys[3]
1515+
);
15081516
let small_thresh_ms: SegwitMiniScript = small_thresh.compile().unwrap();
15091517
let small_thresh_ms_expected: SegwitMiniScript =
1510-
ms_str!("multi({},{},{},{})", k, keys[0], keys[1], keys[2]);
1518+
ms_str!("multi({},{},{},{},{})", k, keys[0], keys[1], keys[2], keys[3]);
15111519
assert_eq!(small_thresh_ms, small_thresh_ms_expected);
15121520
}
15131521

@@ -1640,8 +1648,15 @@ mod tests {
16401648
let small_thresh: Concrete<String> =
16411649
policy_str!("{}", &format!("thresh({},pk(B),pk(C),pk(D))", k));
16421650
let small_thresh_ms: Miniscript<String, Tap> = small_thresh.compile().unwrap();
1643-
let small_thresh_ms_expected: Miniscript<String, Tap> = ms_str!("multi_a({},B,C,D)", k);
1644-
assert_eq!(small_thresh_ms, small_thresh_ms_expected);
1651+
// When k == 3 it is more efficient to use and_v than multi_a
1652+
if k == 3 {
1653+
assert_eq!(
1654+
small_thresh_ms,
1655+
ms_str!("and_v(v:and_v(vc:pk_k(B),c:pk_k(C)),c:pk_k(D))")
1656+
);
1657+
} else {
1658+
assert_eq!(small_thresh_ms, ms_str!("multi_a({},B,C,D)", k));
1659+
}
16451660
}
16461661
}
16471662
}

src/policy/mod.rs

+10-9
Original file line numberDiff line numberDiff line change
@@ -505,12 +505,12 @@ mod tests {
505505
Tr::<String>::from_str(
506506
"tr(UNSPEND ,{
507507
{
508-
{multi_a(7,B,C,D,E,F,G,H),multi_a(7,A,C,D,E,F,G,H)},
509-
{multi_a(7,A,B,D,E,F,G,H),multi_a(7,A,B,C,E,F,G,H)}
508+
{and_v(v:and_v(v:and_v(v:and_v(v:and_v(v:and_v(v:pk(B),pk(C)),pk(D)),pk(E)),pk(F)),pk(G)),pk(H)),and_v(v:and_v(v:and_v(v:and_v(v:and_v(v:and_v(v:pk(A),pk(C)),pk(D)),pk(E)),pk(F)),pk(G)),pk(H))},
509+
{and_v(v:and_v(v:and_v(v:and_v(v:and_v(v:and_v(v:pk(A),pk(B)),pk(D)),pk(E)),pk(F)),pk(G)),pk(H)),and_v(v:and_v(v:and_v(v:and_v(v:and_v(v:and_v(v:pk(A),pk(B)),pk(C)),pk(E)),pk(F)),pk(G)),pk(H))}
510510
},
511511
{
512-
{multi_a(7,A,B,C,D,F,G,H),multi_a(7,A,B,C,D,E,G,H)}
513-
,{multi_a(7,A,B,C,D,E,F,H),multi_a(7,A,B,C,D,E,F,G)}
512+
{and_v(v:and_v(v:and_v(v:and_v(v:and_v(v:and_v(v:pk(A),pk(B)),pk(C)),pk(D)),pk(F)),pk(G)),pk(H)),and_v(v:and_v(v:and_v(v:and_v(v:and_v(v:and_v(v:pk(A),pk(B)),pk(C)),pk(D)),pk(E)),pk(G)),pk(H))},
513+
{and_v(v:and_v(v:and_v(v:and_v(v:and_v(v:and_v(v:pk(A),pk(B)),pk(C)),pk(D)),pk(E)),pk(F)),pk(H)),and_v(v:and_v(v:and_v(v:and_v(v:and_v(v:and_v(v:pk(A),pk(B)),pk(C)),pk(D)),pk(E)),pk(F)),pk(G))}
514514
}})"
515515
.replace(&['\t', ' ', '\n'][..], "")
516516
.as_str(),
@@ -526,18 +526,19 @@ mod tests {
526526
let desc = pol
527527
.compile_tr_private_experimental(Some(unspendable_key.clone()))
528528
.unwrap();
529+
println!("{}", desc);
529530
let expected_desc = Descriptor::Tr(
530531
Tr::<String>::from_str(
531532
"tr(UNSPEND,
532533
{{
533-
{multi_a(3,A,D,E),multi_a(3,A,C,E)},
534-
{multi_a(3,A,C,D),multi_a(3,A,B,E)}\
534+
{and_v(v:and_v(v:pk(A),pk(D)),pk(E)),and_v(v:and_v(v:pk(A),pk(C)),pk(E))},
535+
{and_v(v:and_v(v:pk(A),pk(C)),pk(D)),and_v(v:and_v(v:pk(A),pk(B)),pk(E))}
535536
},
536537
{
537-
{multi_a(3,A,B,D),multi_a(3,A,B,C)},
538+
{and_v(v:and_v(v:pk(A),pk(B)),pk(D)),and_v(v:and_v(v:pk(A),pk(B)),pk(C))},
538539
{
539-
{multi_a(3,C,D,E),multi_a(3,B,D,E)},
540-
{multi_a(3,B,C,E),multi_a(3,B,C,D)}
540+
{and_v(v:and_v(v:pk(C),pk(D)),pk(E)),and_v(v:and_v(v:pk(B),pk(D)),pk(E))},
541+
{and_v(v:and_v(v:pk(B),pk(C)),pk(E)),and_v(v:and_v(v:pk(B),pk(C)),pk(D))}
541542
}}})"
542543
.replace(&['\t', ' ', '\n'][..], "")
543544
.as_str(),

0 commit comments

Comments
 (0)