Skip to content

Commit a307788

Browse files
committed
Add support for sortedmulti_a
Support sortedmulti_a at the top-level of taproot descriptors, which is mostly identical to multi_a, except that the x-only pubkeys are sorted upon encoding into Bitcoin Script.
1 parent 32c096b commit a307788

20 files changed

Lines changed: 223 additions & 34 deletions

File tree

bitcoind-tests/tests/test_desc.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,11 @@ fn test_descs(cl: &Client, testdata: &TestData) {
372372
test_desc_satisfy(cl, testdata, "tr(X!,{pk(X1!),multi_a(3,X2,X3,X4,X5!)})").unwrap();
373373
test_desc_satisfy(cl, testdata, "tr(X!,{pk(X1!),multi_a(4,X2,X3,X4,X5)})").unwrap();
374374

375+
test_desc_satisfy(cl, testdata, "tr(X!,{pk(X1!),sortedmulti_a(1,X2,X3!,X4!,X5!)})").unwrap();
376+
test_desc_satisfy(cl, testdata, "tr(X!,{pk(X1!),sortedmulti_a(2,X2,X3,X4!,X5!)})").unwrap();
377+
test_desc_satisfy(cl, testdata, "tr(X!,{pk(X1!),sortedmulti_a(3,X2,X3,X4,X5!)})").unwrap();
378+
test_desc_satisfy(cl, testdata, "tr(X!,{pk(X1!),sortedmulti_a(4,X2,X3,X4,X5)})").unwrap();
379+
375380
// Test 7: Test script tree of depth 127 is valid, only X128 is known
376381
test_desc_satisfy(cl, testdata, "tr(X!,{pk(X1!),{pk(X2!),{pk(X3!),{pk(X4!),{pk(X5!),{pk(X6!),{pk(X7!),{pk(X8!),{pk(X9!),{pk(X10!),{pk(X11!),{pk(X12!),{pk(X13!),{pk(X14!),{pk(X15!),{pk(X16!),{pk(X17!),{pk(X18!),{pk(X19!),{pk(X20!),{pk(X21!),{pk(X22!),{pk(X23!),{pk(X24!),{pk(X25!),{pk(X26!),{pk(X27!),{pk(X28!),{pk(X29!),{pk(X30!),{pk(X31!),{pk(X32!),{pk(X33!),{pk(X34!),{pk(X35!),{pk(X36!),{pk(X37!),{pk(X38!),{pk(X39!),{pk(X40!),{pk(X41!),{pk(X42!),{pk(X43!),{pk(X44!),{pk(X45!),{pk(X46!),{pk(X47!),{pk(X48!),{pk(X49!),{pk(X50!),{pk(X51!),{pk(X52!),{pk(X53!),{pk(X54!),{pk(X55!),{pk(X56!),{pk(X57!),{pk(X58!),{pk(X59!),{pk(X60!),{pk(X61!),{pk(X62!),{pk(X63!),{pk(X64!),{pk(X65!),{pk(X66!),{pk(X67!),{pk(X68!),{pk(X69!),{pk(X70!),{pk(X71!),{pk(X72!),{pk(X73!),{pk(X74!),{pk(X75!),{pk(X76!),{pk(X77!),{pk(X78!),{pk(X79!),{pk(X80!),{pk(X81!),{pk(X82!),{pk(X83!),{pk(X84!),{pk(X85!),{pk(X86!),{pk(X87!),{pk(X88!),{pk(X89!),{pk(X90!),{pk(X91!),{pk(X92!),{pk(X93!),{pk(X94!),{pk(X95!),{pk(X96!),{pk(X97!),{pk(X98!),{pk(X99!),{pk(X100!),{pk(X101!),{pk(X102!),{pk(X103!),{pk(X104!),{pk(X105!),{pk(X106!),{pk(X107!),{pk(X108!),{pk(X109!),{pk(X110!),{pk(X111!),{pk(X112!),{pk(X113!),{pk(X114!),{pk(X115!),{pk(X116!),{pk(X117!),{pk(X118!),{pk(X119!),{pk(X120!),{pk(X121!),{pk(X122!),{pk(X123!),{pk(X124!),{pk(X125!),{pk(X126!),{pk(X127!),pk(X128)}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}})").unwrap();
377382

examples/xpub_descriptors.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ fn main() {
2020

2121
// P2WSH-P2SH and ranged xpubs.
2222
let _ = p2sh_p2wsh(&secp);
23+
24+
// P2TR with xpubs in sortedmulti_a
25+
let _ = p2tr_sortedmulti_a(&secp);
2326
}
2427

2528
/// Parses a P2WSH descriptor, returns the associated address.
@@ -64,3 +67,34 @@ fn p2sh_p2wsh<C: Verification>(secp: &Secp256k1<C>) -> Address {
6467
assert_eq!(address, expected);
6568
address
6669
}
70+
71+
/// Parses a P2TR sortedmulti_a descriptor, returns the associated address.
72+
fn p2tr_sortedmulti_a<C: Verification>(secp: &Secp256k1<C>) -> Address {
73+
let internal = "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0";
74+
// It does not matter what order the two xpubs go in, the same address will be generated.
75+
let s1 = format!("tr({},sortedmulti_a(2,{}/1/0/*,{}/0/0/*))", internal, XPUB_1, XPUB_2);
76+
let s2 = format!("tr({},sortedmulti_a(2,{}/0/0/*,{}/1/0/*))", internal, XPUB_2, XPUB_1);
77+
78+
let [address1, address2]: [Address; 2] = [s1, s2]
79+
.into_iter()
80+
.map(|s| {
81+
Descriptor::<DescriptorPublicKey>::from_str(&s)
82+
.unwrap()
83+
.derived_descriptor(secp, 5)
84+
.unwrap()
85+
.address(Network::Bitcoin)
86+
.unwrap()
87+
})
88+
.collect::<Vec<_>>()
89+
.try_into()
90+
.unwrap();
91+
92+
let expected =
93+
Address::from_str("bc1ppfd3y5lxq4nf3tfstccz0t0hly3vmj93t7z46e52zlpt6dyf4hwqxaxnxc")
94+
.unwrap()
95+
.require_network(Network::Bitcoin)
96+
.unwrap();
97+
assert_eq!(address1, expected);
98+
assert_eq!(address1, address2);
99+
address1
100+
}

src/descriptor/sortedmulti.rs

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -123,16 +123,7 @@ impl<Pk: MiniscriptKey, Ctx: ScriptContext> SortedMultiVec<Pk, Ctx> {
123123
where
124124
Pk: ToPublicKey,
125125
{
126-
let mut thresh = self.inner.clone();
127-
// Sort pubkeys lexicographically according to BIP 67
128-
thresh.data_mut().sort_by(|a, b| {
129-
a.to_public_key()
130-
.inner
131-
.serialize()
132-
.partial_cmp(&b.to_public_key().inner.serialize())
133-
.unwrap()
134-
});
135-
Terminal::Multi(thresh)
126+
Terminal::Multi(self.inner.to_sorted())
136127
}
137128

138129
/// Encode as a Bitcoin script

src/interpreter/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -868,7 +868,7 @@ where
868868
None => return Some(Err(Error::UnexpectedStackEnd)),
869869
}
870870
}
871-
Terminal::MultiA(ref thresh) => {
871+
Terminal::MultiA(ref thresh) | Terminal::SortedMultiA(ref thresh) => {
872872
if node_state.n_evaluated == thresh.n() {
873873
if node_state.n_satisfied == thresh.k() {
874874
self.stack.push(stack::Element::Satisfied);

src/iter/mod.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ impl<'a, Pk: MiniscriptKey, Ctx: ScriptContext> TreeLike for &'a Miniscript<Pk,
2727
use Terminal::*;
2828
match self.node {
2929
PkK(..) | PkH(..) | RawPkH(..) | After(..) | Older(..) | Sha256(..) | Hash256(..)
30-
| Ripemd160(..) | Hash160(..) | True | False | Multi(..) | MultiA(..) => Tree::Nullary,
30+
| Ripemd160(..) | Hash160(..) | True | False | Multi(..) | MultiA(..)
31+
| SortedMultiA(..) => Tree::Nullary,
3132
Alt(ref sub)
3233
| Swap(ref sub)
3334
| Check(ref sub)
@@ -57,7 +58,8 @@ impl<'a, Pk: MiniscriptKey, Ctx: ScriptContext> TreeLike for &'a Arc<Miniscript<
5758
use Terminal::*;
5859
match self.node {
5960
PkK(..) | PkH(..) | RawPkH(..) | After(..) | Older(..) | Sha256(..) | Hash256(..)
60-
| Ripemd160(..) | Hash160(..) | True | False | Multi(..) | MultiA(..) => Tree::Nullary,
61+
| Ripemd160(..) | Hash160(..) | True | False | Multi(..) | MultiA(..)
62+
| SortedMultiA(..) => Tree::Nullary,
6163
Alt(ref sub)
6264
| Swap(ref sub)
6365
| Check(ref sub)
@@ -87,7 +89,8 @@ impl<'a, Pk: MiniscriptKey, Ctx: ScriptContext> TreeLike for &'a Terminal<Pk, Ct
8789
use Terminal::*;
8890
match self {
8991
PkK(..) | PkH(..) | RawPkH(..) | After(..) | Older(..) | Sha256(..) | Hash256(..)
90-
| Ripemd160(..) | Hash160(..) | True | False | Multi(..) | MultiA(..) => Tree::Nullary,
92+
| Ripemd160(..) | Hash160(..) | True | False | Multi(..) | MultiA(..)
93+
| SortedMultiA(..) => Tree::Nullary,
9194
Alt(ref sub)
9295
| Swap(ref sub)
9396
| Check(ref sub)

src/miniscript/astelem.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -162,12 +162,22 @@ impl<Pk: MiniscriptKey, Ctx: ScriptContext> Terminal<Pk, Ctx> {
162162
.push_int(thresh.n() as i64)
163163
.push_opcode(opcodes::all::OP_CHECKMULTISIG)
164164
}
165-
Terminal::MultiA(ref thresh) => {
165+
Terminal::MultiA(ref thresh) | Terminal::SortedMultiA(ref thresh) => {
166166
debug_assert!(Ctx::sig_type() == SigType::Schnorr);
167-
// keys must be atleast len 1 here, guaranteed by typing rules
168-
builder = builder.push_ms_key::<_, Ctx>(&thresh.data()[0]);
167+
let sorted;
168+
let mut iter = if let Terminal::SortedMultiA(thresh) = self {
169+
sorted = thresh.to_sorted_xonly();
170+
sorted.iter()
171+
} else {
172+
thresh.iter()
173+
};
174+
builder =
175+
builder.push_ms_key::<_, Ctx>(iter.next().expect(
176+
"multi_a keys must be atleast len 1 here, guaranteed by typing rules",
177+
));
169178
builder = builder.push_opcode(opcodes::all::OP_CHECKSIG);
170-
for pk in thresh.iter().skip(1) {
179+
180+
for pk in iter {
171181
builder = builder.push_ms_key::<_, Ctx>(pk);
172182
builder = builder.push_opcode(opcodes::all::OP_CHECKSIGADD);
173183
}

src/miniscript/context.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,9 @@ impl ScriptContext for Legacy {
390390
}
391391
Ok(())
392392
}
393-
Terminal::MultiA(..) => Err(ScriptContextError::MultiANotAllowed),
393+
Terminal::MultiA(..) | Terminal::SortedMultiA(..) => {
394+
Err(ScriptContextError::MultiANotAllowed)
395+
}
394396
_ => Ok(()),
395397
};
396398
// 2. After fragment and param check, validate the script size finally
@@ -494,7 +496,9 @@ impl ScriptContext for Segwitv0 {
494496
}
495497
Ok(())
496498
}
497-
Terminal::MultiA(..) => Err(ScriptContextError::MultiANotAllowed),
499+
Terminal::MultiA(..) | Terminal::SortedMultiA(..) => {
500+
Err(ScriptContextError::MultiANotAllowed)
501+
}
498502
_ => Ok(()),
499503
};
500504
// 2. After fragment and param check, validate the script size finally
@@ -599,7 +603,7 @@ impl ScriptContext for Tap {
599603
// 1. Check the node first, throw an error on the language itself
600604
let node_checked = match ms.node {
601605
Terminal::PkK(ref pk) => Self::check_pk(pk),
602-
Terminal::MultiA(ref thresh) => {
606+
Terminal::MultiA(ref thresh) | Terminal::SortedMultiA(ref thresh) => {
603607
for pk in thresh.iter() {
604608
Self::check_pk(pk)?;
605609
}
@@ -716,7 +720,9 @@ impl ScriptContext for BareCtx {
716720
}
717721
Ok(())
718722
}
719-
Terminal::MultiA(..) => Err(ScriptContextError::MultiANotAllowed),
723+
Terminal::MultiA(..) | Terminal::SortedMultiA(..) => {
724+
Err(ScriptContextError::MultiANotAllowed)
725+
}
720726
_ => Ok(()),
721727
};
722728
// 2. After fragment and param check, validate the script size finally

src/miniscript/decode.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@ pub enum Terminal<Pk: MiniscriptKey, Ctx: ScriptContext> {
155155
Multi(Threshold<Pk, MAX_PUBKEYS_PER_MULTISIG>),
156156
/// `<key> CHECKSIG (<key> CHECKSIGADD)*(n-1) k NUMEQUAL`
157157
MultiA(Threshold<Pk, MAX_PUBKEYS_IN_CHECKSIGADD>),
158+
/// `<key> CHECKSIG (<key> CHECKSIGADD)*(n-1) k NUMEQUAL`
159+
SortedMultiA(Threshold<Pk, MAX_PUBKEYS_IN_CHECKSIGADD>),
158160
}
159161

160162
impl<Pk: MiniscriptKey, Ctx: ScriptContext> Clone for Terminal<Pk, Ctx> {
@@ -213,6 +215,7 @@ impl<Pk: MiniscriptKey, Ctx: ScriptContext> Clone for Terminal<Pk, Ctx> {
213215
}
214216
Terminal::Multi(ref thresh) => Terminal::Multi(thresh.clone()),
215217
Terminal::MultiA(ref thresh) => Terminal::MultiA(thresh.clone()),
218+
Terminal::SortedMultiA(ref thresh) => Terminal::SortedMultiA(thresh.clone()),
216219
}
217220
}
218221
}
@@ -232,6 +235,9 @@ impl<Pk: MiniscriptKey, Ctx: ScriptContext> PartialEq for Terminal<Pk, Ctx> {
232235
(Terminal::Hash160(h1), Terminal::Hash160(h2)) if h1 != h2 => return false,
233236
(Terminal::Multi(th1), Terminal::Multi(th2)) if th1 != th2 => return false,
234237
(Terminal::MultiA(th1), Terminal::MultiA(th2)) if th1 != th2 => return false,
238+
(Terminal::SortedMultiA(th1), Terminal::SortedMultiA(th2)) if th1 != th2 => {
239+
return false
240+
}
235241
_ => {
236242
if mem::discriminant(me) != mem::discriminant(you) {
237243
return false;
@@ -264,7 +270,7 @@ impl<Pk: MiniscriptKey, Ctx: ScriptContext> core::hash::Hash for Terminal<Pk, Ct
264270
// The actual children will be hashed when we iterate
265271
}
266272
Terminal::Multi(th) => th.hash(hasher),
267-
Terminal::MultiA(th) => th.hash(hasher),
273+
Terminal::MultiA(th) | Terminal::SortedMultiA(th) => th.hash(hasher),
268274
_ => {}
269275
}
270276
}
@@ -553,7 +559,11 @@ pub fn decode<Ctx: ScriptContext>(
553559
);
554560
keys.reverse();
555561
let thresh = Threshold::new(k as usize, keys).map_err(Error::Threshold)?;
556-
term.push(Miniscript::multi_a(thresh));
562+
if thresh.is_sorted_xonly() {
563+
term.push(Miniscript::sortedmulti_a(thresh));
564+
} else {
565+
term.push(Miniscript::multi_a(thresh));
566+
}
557567
},
558568
);
559569
}

src/miniscript/display.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ impl<'a, Pk: MiniscriptKey, Ctx: ScriptContext> TreeLike for DisplayNode<'a, Pk,
130130
Terminal::MultiA(ref thresh) => {
131131
Tree::Nary(NaryChildren::Keys(thresh.k(), thresh.data()))
132132
}
133+
Terminal::SortedMultiA(thresh) => {
134+
Tree::Nary(NaryChildren::Keys(thresh.k(), thresh.data()))
135+
}
133136
},
134137
// Only nodes have children; the rest are terminals.
135138
_ => Tree::Nullary,
@@ -272,6 +275,7 @@ impl<Pk: MiniscriptKey, Ctx: ScriptContext> Terminal<Pk, Ctx> {
272275
Terminal::Thresh(..) => "thresh",
273276
Terminal::Multi(..) => "multi",
274277
Terminal::MultiA(..) => "multi_a",
278+
Terminal::SortedMultiA(..) => "sortedmulti_a",
275279
}
276280
}
277281

src/miniscript/iter.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ impl<Pk: MiniscriptKey, Ctx: ScriptContext> Miniscript<Pk, Ctx> {
9797
(Terminal::PkK(key), 0) | (Terminal::PkH(key), 0) => Some(key.clone()),
9898
(Terminal::Multi(thresh), _) => thresh.data().get(n).cloned(),
9999
(Terminal::MultiA(thresh), _) => thresh.data().get(n).cloned(),
100+
(Terminal::SortedMultiA(thresh), _) => thresh.data().get(n).cloned(),
100101
_ => None,
101102
}
102103
}

0 commit comments

Comments
 (0)