From a307788e214bfe5619e615239f6b88441209bf3b Mon Sep 17 00:00:00 2001 From: Trevor Arjeski Date: Sun, 22 Mar 2026 12:12:58 +0000 Subject: [PATCH] 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. --- bitcoind-tests/tests/test_desc.rs | 5 +++ examples/xpub_descriptors.rs | 34 +++++++++++++++++++ src/descriptor/sortedmulti.rs | 11 +----- src/interpreter/mod.rs | 2 +- src/iter/mod.rs | 9 +++-- src/miniscript/astelem.rs | 18 +++++++--- src/miniscript/context.rs | 14 +++++--- src/miniscript/decode.rs | 14 ++++++-- src/miniscript/display.rs | 4 +++ src/miniscript/iter.rs | 1 + src/miniscript/mod.rs | 51 ++++++++++++++++++++++++++-- src/miniscript/satisfy.rs | 4 +-- src/miniscript/types/correctness.rs | 5 +++ src/miniscript/types/extra_props.rs | 4 +++ src/miniscript/types/malleability.rs | 5 +++ src/miniscript/types/mod.rs | 6 ++++ src/policy/compiler.rs | 3 ++ src/policy/mod.rs | 12 ++++--- src/primitives/threshold.rs | 38 +++++++++++++++++++++ src/util.rs | 17 ++++++++++ 20 files changed, 223 insertions(+), 34 deletions(-) diff --git a/bitcoind-tests/tests/test_desc.rs b/bitcoind-tests/tests/test_desc.rs index ea1581d97..4c6d499cf 100644 --- a/bitcoind-tests/tests/test_desc.rs +++ b/bitcoind-tests/tests/test_desc.rs @@ -372,6 +372,11 @@ fn test_descs(cl: &Client, testdata: &TestData) { test_desc_satisfy(cl, testdata, "tr(X!,{pk(X1!),multi_a(3,X2,X3,X4,X5!)})").unwrap(); test_desc_satisfy(cl, testdata, "tr(X!,{pk(X1!),multi_a(4,X2,X3,X4,X5)})").unwrap(); + test_desc_satisfy(cl, testdata, "tr(X!,{pk(X1!),sortedmulti_a(1,X2,X3!,X4!,X5!)})").unwrap(); + test_desc_satisfy(cl, testdata, "tr(X!,{pk(X1!),sortedmulti_a(2,X2,X3,X4!,X5!)})").unwrap(); + test_desc_satisfy(cl, testdata, "tr(X!,{pk(X1!),sortedmulti_a(3,X2,X3,X4,X5!)})").unwrap(); + test_desc_satisfy(cl, testdata, "tr(X!,{pk(X1!),sortedmulti_a(4,X2,X3,X4,X5)})").unwrap(); + // Test 7: Test script tree of depth 127 is valid, only X128 is known 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(); diff --git a/examples/xpub_descriptors.rs b/examples/xpub_descriptors.rs index 2f9478ff8..d7be9cc4f 100644 --- a/examples/xpub_descriptors.rs +++ b/examples/xpub_descriptors.rs @@ -20,6 +20,9 @@ fn main() { // P2WSH-P2SH and ranged xpubs. let _ = p2sh_p2wsh(&secp); + + // P2TR with xpubs in sortedmulti_a + let _ = p2tr_sortedmulti_a(&secp); } /// Parses a P2WSH descriptor, returns the associated address. @@ -64,3 +67,34 @@ fn p2sh_p2wsh(secp: &Secp256k1) -> Address { assert_eq!(address, expected); address } + +/// Parses a P2TR sortedmulti_a descriptor, returns the associated address. +fn p2tr_sortedmulti_a(secp: &Secp256k1) -> Address { + let internal = "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"; + // It does not matter what order the two xpubs go in, the same address will be generated. + let s1 = format!("tr({},sortedmulti_a(2,{}/1/0/*,{}/0/0/*))", internal, XPUB_1, XPUB_2); + let s2 = format!("tr({},sortedmulti_a(2,{}/0/0/*,{}/1/0/*))", internal, XPUB_2, XPUB_1); + + let [address1, address2]: [Address; 2] = [s1, s2] + .into_iter() + .map(|s| { + Descriptor::::from_str(&s) + .unwrap() + .derived_descriptor(secp, 5) + .unwrap() + .address(Network::Bitcoin) + .unwrap() + }) + .collect::>() + .try_into() + .unwrap(); + + let expected = + Address::from_str("bc1ppfd3y5lxq4nf3tfstccz0t0hly3vmj93t7z46e52zlpt6dyf4hwqxaxnxc") + .unwrap() + .require_network(Network::Bitcoin) + .unwrap(); + assert_eq!(address1, expected); + assert_eq!(address1, address2); + address1 +} diff --git a/src/descriptor/sortedmulti.rs b/src/descriptor/sortedmulti.rs index 5ba952ff0..cc75063fc 100644 --- a/src/descriptor/sortedmulti.rs +++ b/src/descriptor/sortedmulti.rs @@ -123,16 +123,7 @@ impl SortedMultiVec { where Pk: ToPublicKey, { - let mut thresh = self.inner.clone(); - // Sort pubkeys lexicographically according to BIP 67 - thresh.data_mut().sort_by(|a, b| { - a.to_public_key() - .inner - .serialize() - .partial_cmp(&b.to_public_key().inner.serialize()) - .unwrap() - }); - Terminal::Multi(thresh) + Terminal::Multi(self.inner.to_sorted()) } /// Encode as a Bitcoin script diff --git a/src/interpreter/mod.rs b/src/interpreter/mod.rs index 1d6ed9695..ca88adfc9 100644 --- a/src/interpreter/mod.rs +++ b/src/interpreter/mod.rs @@ -868,7 +868,7 @@ where None => return Some(Err(Error::UnexpectedStackEnd)), } } - Terminal::MultiA(ref thresh) => { + Terminal::MultiA(ref thresh) | Terminal::SortedMultiA(ref thresh) => { if node_state.n_evaluated == thresh.n() { if node_state.n_satisfied == thresh.k() { self.stack.push(stack::Element::Satisfied); diff --git a/src/iter/mod.rs b/src/iter/mod.rs index abb8eda7b..82ce9a25f 100644 --- a/src/iter/mod.rs +++ b/src/iter/mod.rs @@ -27,7 +27,8 @@ impl<'a, Pk: MiniscriptKey, Ctx: ScriptContext> TreeLike for &'a Miniscript Tree::Nullary, + | Ripemd160(..) | Hash160(..) | True | False | Multi(..) | MultiA(..) + | SortedMultiA(..) => Tree::Nullary, Alt(ref sub) | Swap(ref sub) | Check(ref sub) @@ -57,7 +58,8 @@ impl<'a, Pk: MiniscriptKey, Ctx: ScriptContext> TreeLike for &'a Arc Tree::Nullary, + | Ripemd160(..) | Hash160(..) | True | False | Multi(..) | MultiA(..) + | SortedMultiA(..) => Tree::Nullary, Alt(ref sub) | Swap(ref sub) | Check(ref sub) @@ -87,7 +89,8 @@ impl<'a, Pk: MiniscriptKey, Ctx: ScriptContext> TreeLike for &'a Terminal Tree::Nullary, + | Ripemd160(..) | Hash160(..) | True | False | Multi(..) | MultiA(..) + | SortedMultiA(..) => Tree::Nullary, Alt(ref sub) | Swap(ref sub) | Check(ref sub) diff --git a/src/miniscript/astelem.rs b/src/miniscript/astelem.rs index 19611b083..78f3cfc33 100644 --- a/src/miniscript/astelem.rs +++ b/src/miniscript/astelem.rs @@ -162,12 +162,22 @@ impl Terminal { .push_int(thresh.n() as i64) .push_opcode(opcodes::all::OP_CHECKMULTISIG) } - Terminal::MultiA(ref thresh) => { + Terminal::MultiA(ref thresh) | Terminal::SortedMultiA(ref thresh) => { debug_assert!(Ctx::sig_type() == SigType::Schnorr); - // keys must be atleast len 1 here, guaranteed by typing rules - builder = builder.push_ms_key::<_, Ctx>(&thresh.data()[0]); + let sorted; + let mut iter = if let Terminal::SortedMultiA(thresh) = self { + sorted = thresh.to_sorted_xonly(); + sorted.iter() + } else { + thresh.iter() + }; + builder = + builder.push_ms_key::<_, Ctx>(iter.next().expect( + "multi_a keys must be atleast len 1 here, guaranteed by typing rules", + )); builder = builder.push_opcode(opcodes::all::OP_CHECKSIG); - for pk in thresh.iter().skip(1) { + + for pk in iter { builder = builder.push_ms_key::<_, Ctx>(pk); builder = builder.push_opcode(opcodes::all::OP_CHECKSIGADD); } diff --git a/src/miniscript/context.rs b/src/miniscript/context.rs index c6c4955a0..3801d35f5 100644 --- a/src/miniscript/context.rs +++ b/src/miniscript/context.rs @@ -390,7 +390,9 @@ impl ScriptContext for Legacy { } Ok(()) } - Terminal::MultiA(..) => Err(ScriptContextError::MultiANotAllowed), + Terminal::MultiA(..) | Terminal::SortedMultiA(..) => { + Err(ScriptContextError::MultiANotAllowed) + } _ => Ok(()), }; // 2. After fragment and param check, validate the script size finally @@ -494,7 +496,9 @@ impl ScriptContext for Segwitv0 { } Ok(()) } - Terminal::MultiA(..) => Err(ScriptContextError::MultiANotAllowed), + Terminal::MultiA(..) | Terminal::SortedMultiA(..) => { + Err(ScriptContextError::MultiANotAllowed) + } _ => Ok(()), }; // 2. After fragment and param check, validate the script size finally @@ -599,7 +603,7 @@ impl ScriptContext for Tap { // 1. Check the node first, throw an error on the language itself let node_checked = match ms.node { Terminal::PkK(ref pk) => Self::check_pk(pk), - Terminal::MultiA(ref thresh) => { + Terminal::MultiA(ref thresh) | Terminal::SortedMultiA(ref thresh) => { for pk in thresh.iter() { Self::check_pk(pk)?; } @@ -716,7 +720,9 @@ impl ScriptContext for BareCtx { } Ok(()) } - Terminal::MultiA(..) => Err(ScriptContextError::MultiANotAllowed), + Terminal::MultiA(..) | Terminal::SortedMultiA(..) => { + Err(ScriptContextError::MultiANotAllowed) + } _ => Ok(()), }; // 2. After fragment and param check, validate the script size finally diff --git a/src/miniscript/decode.rs b/src/miniscript/decode.rs index 213ac8266..3b48e956b 100644 --- a/src/miniscript/decode.rs +++ b/src/miniscript/decode.rs @@ -155,6 +155,8 @@ pub enum Terminal { Multi(Threshold), /// ` CHECKSIG ( CHECKSIGADD)*(n-1) k NUMEQUAL` MultiA(Threshold), + /// ` CHECKSIG ( CHECKSIGADD)*(n-1) k NUMEQUAL` + SortedMultiA(Threshold), } impl Clone for Terminal { @@ -213,6 +215,7 @@ impl Clone for Terminal { } Terminal::Multi(ref thresh) => Terminal::Multi(thresh.clone()), Terminal::MultiA(ref thresh) => Terminal::MultiA(thresh.clone()), + Terminal::SortedMultiA(ref thresh) => Terminal::SortedMultiA(thresh.clone()), } } } @@ -232,6 +235,9 @@ impl PartialEq for Terminal { (Terminal::Hash160(h1), Terminal::Hash160(h2)) if h1 != h2 => return false, (Terminal::Multi(th1), Terminal::Multi(th2)) if th1 != th2 => return false, (Terminal::MultiA(th1), Terminal::MultiA(th2)) if th1 != th2 => return false, + (Terminal::SortedMultiA(th1), Terminal::SortedMultiA(th2)) if th1 != th2 => { + return false + } _ => { if mem::discriminant(me) != mem::discriminant(you) { return false; @@ -264,7 +270,7 @@ impl core::hash::Hash for Terminal th.hash(hasher), - Terminal::MultiA(th) => th.hash(hasher), + Terminal::MultiA(th) | Terminal::SortedMultiA(th) => th.hash(hasher), _ => {} } } @@ -553,7 +559,11 @@ pub fn decode( ); keys.reverse(); let thresh = Threshold::new(k as usize, keys).map_err(Error::Threshold)?; - term.push(Miniscript::multi_a(thresh)); + if thresh.is_sorted_xonly() { + term.push(Miniscript::sortedmulti_a(thresh)); + } else { + term.push(Miniscript::multi_a(thresh)); + } }, ); } diff --git a/src/miniscript/display.rs b/src/miniscript/display.rs index c6510b7d0..c0da63160 100644 --- a/src/miniscript/display.rs +++ b/src/miniscript/display.rs @@ -130,6 +130,9 @@ impl<'a, Pk: MiniscriptKey, Ctx: ScriptContext> TreeLike for DisplayNode<'a, Pk, Terminal::MultiA(ref thresh) => { Tree::Nary(NaryChildren::Keys(thresh.k(), thresh.data())) } + Terminal::SortedMultiA(thresh) => { + Tree::Nary(NaryChildren::Keys(thresh.k(), thresh.data())) + } }, // Only nodes have children; the rest are terminals. _ => Tree::Nullary, @@ -272,6 +275,7 @@ impl Terminal { Terminal::Thresh(..) => "thresh", Terminal::Multi(..) => "multi", Terminal::MultiA(..) => "multi_a", + Terminal::SortedMultiA(..) => "sortedmulti_a", } } diff --git a/src/miniscript/iter.rs b/src/miniscript/iter.rs index 0ac48e651..2bfcb7008 100644 --- a/src/miniscript/iter.rs +++ b/src/miniscript/iter.rs @@ -97,6 +97,7 @@ impl Miniscript { (Terminal::PkK(key), 0) | (Terminal::PkH(key), 0) => Some(key.clone()), (Terminal::Multi(thresh), _) => thresh.data().get(n).cloned(), (Terminal::MultiA(thresh), _) => thresh.data().get(n).cloned(), + (Terminal::SortedMultiA(thresh), _) => thresh.data().get(n).cloned(), _ => None, } } diff --git a/src/miniscript/mod.rs b/src/miniscript/mod.rs index 5c666ce59..3dc13da4c 100644 --- a/src/miniscript/mod.rs +++ b/src/miniscript/mod.rs @@ -126,6 +126,7 @@ mod private { } Terminal::Multi(ref thresh) => Terminal::Multi(thresh.clone()), Terminal::MultiA(ref thresh) => Terminal::MultiA(thresh.clone()), + Terminal::SortedMultiA(ref thresh) => Terminal::SortedMultiA(thresh.clone()), }; stack.push(Arc::new(Miniscript { @@ -292,6 +293,17 @@ mod private { } } + // non-const because Thresh::n is not because Vec::len is not + /// The `sortedmulti_a` combinator. + pub fn sortedmulti_a(thresh: crate::Threshold) -> Self { + Self { + ty: types::Type::sortedmulti_a(), + ext: types::extra_props::ExtData::sortedmulti_a(thresh.k(), thresh.n()), + node: Terminal::SortedMultiA(thresh), + phantom: PhantomData, + } + } + /// Add type information(Type and Extdata) to Miniscript based on /// `AstElem` fragment. Dependent on display and clone because of Error /// Display code of type_check. @@ -383,7 +395,7 @@ impl Miniscript { + script_num_size(thresh.n()) + thresh.iter().map(|pk| Ctx::pk_len(pk)).sum::() } - Terminal::MultiA(ref thresh) => { + Terminal::MultiA(ref thresh) | Terminal::SortedMultiA(ref thresh) => { script_num_size(thresh.k()) + 1 // NUMEQUAL + thresh.iter().map(|pk| Ctx::pk_len(pk)).sum::() // n keys @@ -639,7 +651,9 @@ impl ForEachKey for Miniscript { return false; } - Terminal::MultiA(ref thresh) if !thresh.iter().all(&mut pred) => { + Terminal::MultiA(ref thresh) | Terminal::SortedMultiA(ref thresh) + if !thresh.iter().all(&mut pred) => + { return false; } _ => {} @@ -721,6 +735,9 @@ impl Miniscript { Terminal::MultiA(ref thresh) => { Terminal::MultiA(thresh.translate_ref(|k| t.pk(k))?) } + Terminal::SortedMultiA(ref thresh) => { + Terminal::SortedMultiA(thresh.translate_ref(|k| t.pk(k))?) + } }; let new_ms = Miniscript::from_ast(new_term).map_err(TranslateErr::OuterError)?; translated.push(Arc::new(new_ms)); @@ -772,6 +789,7 @@ impl Miniscript { } Terminal::Multi(ref thresh) => Terminal::Multi(thresh.clone()), Terminal::MultiA(ref thresh) => Terminal::MultiA(thresh.clone()), + Terminal::SortedMultiA(ref thresh) => Terminal::SortedMultiA(thresh.clone()), }; stack.push(Arc::new(Miniscript::from_components_unchecked( @@ -865,7 +883,7 @@ impl FromTree for Miniscript { .map_err(From::from) .map_err(Error::Parse)?; - if parent_name == "multi" || parent_name == "multi_a" { + if matches!(parent_name, "multi" | "multi_a" | "sortedmulti_a") { continue; } if parent_name == "thresh" && node.is_first_child() { @@ -967,6 +985,10 @@ impl FromTree for Miniscript { .verify_threshold(|sub| sub.verify_terminal("public_key").map_err(Error::Parse)) .map(Terminal::MultiA) .and_then(Miniscript::from_ast), + "sortedmulti_a" => node + .verify_threshold(|sub| sub.verify_terminal("public_key").map_err(Error::Parse)) + .map(Terminal::SortedMultiA) + .and_then(Miniscript::from_ast), x => { Err(Error::Parse(crate::ParseError::Tree(crate::ParseTreeError::UnknownName { name: x.to_owned(), @@ -1593,24 +1615,42 @@ mod tests { type Segwitv0Ms = Miniscript; type TapMs = Miniscript; let segwit_multi_a_ms = Segwitv0Ms::from_str_insane("multi_a(1,A,B,C)"); + let segwit_sortedmulti_a_ms = Segwitv0Ms::from_str_insane("sortedmulti_a(1,A,B,C)"); assert_eq!( segwit_multi_a_ms.unwrap_err().to_string(), "Multi a(CHECKSIGADD) only allowed post tapscript" ); + assert_eq!( + segwit_sortedmulti_a_ms.unwrap_err().to_string(), + "Multi a(CHECKSIGADD) only allowed post tapscript" + ); let tap_multi_a_ms = TapMs::from_str_insane("multi_a(1,A,B,C)").unwrap(); + let tap_sortedmulti_a_ms = TapMs::from_str_insane("sortedmulti_a(1,A,B,C)").unwrap(); assert_eq!(tap_multi_a_ms.to_string(), "multi_a(1,A,B,C)"); + assert_eq!(tap_sortedmulti_a_ms.to_string(), "sortedmulti_a(1,A,B,C)"); // Test encode/decode and translation tests let tap_ms = tap_multi_a_ms .translate_pk(&mut StrXOnlyKeyTranslator::new()) .unwrap(); + let tap_ms_sorted = tap_sortedmulti_a_ms + .translate_pk(&mut StrXOnlyKeyTranslator::new()) + .unwrap(); // script rtt test assert_eq!( Miniscript::::decode_consensus(&tap_ms.encode()).unwrap(), tap_ms ); + // This won't work cause we won't ever be able to deduce the original + // ordering of keys in the descriptor + // assert_eq!( + // Miniscript::::decode_consensus(&tap_ms_sorted.encode()).unwrap(), + // tap_ms_sorted + // ); assert_eq!(tap_ms.script_size(), 104); + assert_eq!(tap_ms_sorted.script_size(), 104); assert_eq!(tap_ms.encode().len(), tap_ms.script_size()); + assert_eq!(tap_ms_sorted.encode().len(), tap_ms_sorted.script_size()); // Test satisfaction code struct SimpleSatisfier(secp256k1::schnorr::Signature); @@ -1632,11 +1672,16 @@ mod tests { let schnorr_sig = secp256k1::schnorr::Signature::from_str("84526253c27c7aef56c7b71a5cd25bebb66dddda437826defc5b2568bde81f0784526253c27c7aef56c7b71a5cd25bebb66dddda437826defc5b2568bde81f07").unwrap(); let s = SimpleSatisfier(schnorr_sig); let template = tap_ms.build_template(&s); + let template_sorted = tap_ms_sorted.build_template(&s); assert_eq!(template.absolute_timelock, None); + assert_eq!(template_sorted.absolute_timelock, None); assert_eq!(template.relative_timelock, None); + assert_eq!(template_sorted.relative_timelock, None); let wit = tap_ms.satisfy(&s).unwrap(); + let wit_sorted = tap_ms_sorted.satisfy(&s).unwrap(); assert_eq!(wit, vec![schnorr_sig.as_ref().to_vec(), vec![], vec![]]); + assert_eq!(wit_sorted, vec![schnorr_sig.as_ref().to_vec(), vec![], vec![]]); } #[test] diff --git a/src/miniscript/satisfy.rs b/src/miniscript/satisfy.rs index 027aabd7f..5dd12f865 100644 --- a/src/miniscript/satisfy.rs +++ b/src/miniscript/satisfy.rs @@ -1524,7 +1524,7 @@ impl Satisfaction> { } } } - Terminal::MultiA(ref thresh) => { + Terminal::MultiA(ref thresh) | Terminal::SortedMultiA(ref thresh) => { // Collect all available signatures let mut sig_count = 0; let mut sigs = vec![vec![Placeholder::PushZero]; thresh.n()]; @@ -1743,7 +1743,7 @@ impl Satisfaction> { relative_timelock: None, absolute_timelock: None, }, - Terminal::MultiA(ref thresh) => Satisfaction { + Terminal::MultiA(ref thresh) | Terminal::SortedMultiA(ref thresh) => Satisfaction { stack: Witness::Stack(vec![Placeholder::PushZero; thresh.n()]), has_sig: false, relative_timelock: None, diff --git a/src/miniscript/types/correctness.rs b/src/miniscript/types/correctness.rs index de08001dc..638aa4468 100644 --- a/src/miniscript/types/correctness.rs +++ b/src/miniscript/types/correctness.rs @@ -157,6 +157,11 @@ impl Correctness { Correctness { base: Base::B, input: Input::Any, dissatisfiable: true, unit: true } } + /// Constructor for the correctness properties of the `sortedmulti_a` fragment. + pub const fn sortedmulti_a() -> Self { + Correctness { base: Base::B, input: Input::Any, dissatisfiable: true, unit: true } + } + /// Constructor for the correctness properties of any of the hash fragments. pub const fn hash() -> Self { Correctness { base: Base::B, input: Input::OneNonZero, dissatisfiable: true, unit: true } diff --git a/src/miniscript/types/extra_props.rs b/src/miniscript/types/extra_props.rs index defe31540..7ad83f30f 100644 --- a/src/miniscript/types/extra_props.rs +++ b/src/miniscript/types/extra_props.rs @@ -341,6 +341,9 @@ impl ExtData { } } + /// Extra properties for the `sortedmulti_a` fragment. + pub fn sortedmulti_a(k: usize, n: usize) -> Self { Self::multi_a(k, n) } + /// Extra properties for the `sha256` fragment. pub const fn sha256() -> Self { ExtData { @@ -947,6 +950,7 @@ impl ExtData { Terminal::RawPkH(..) => Self::pk_h::(None), Terminal::Multi(ref thresh) => Self::multi(thresh), Terminal::MultiA(ref thresh) => Self::multi_a(thresh.k(), thresh.n()), + Terminal::SortedMultiA(ref thresh) => Self::multi_a(thresh.k(), thresh.n()), Terminal::After(t) => Self::after(t), Terminal::Older(t) => Self::older(t), Terminal::Sha256(..) => Self::sha256(), diff --git a/src/miniscript/types/malleability.rs b/src/miniscript/types/malleability.rs index 2c15127df..70a6f25f4 100644 --- a/src/miniscript/types/malleability.rs +++ b/src/miniscript/types/malleability.rs @@ -109,6 +109,11 @@ impl Malleability { Malleability { dissat: Dissat::Unique, safe: true, non_malleable: true } } + /// Constructor for the malleabilitiy properties of the `sortedmulti_a` fragment. + pub const fn sortedmulti_a() -> Self { + Malleability { dissat: Dissat::Unique, safe: true, non_malleable: true } + } + /// Constructor for the malleabilitiy properties of any of the hash fragments. pub const fn hash() -> Self { Malleability { dissat: Dissat::Unknown, safe: false, non_malleable: true } diff --git a/src/miniscript/types/mod.rs b/src/miniscript/types/mod.rs index 1b1cb2e85..6ebf59900 100644 --- a/src/miniscript/types/mod.rs +++ b/src/miniscript/types/mod.rs @@ -234,6 +234,11 @@ impl Type { Type { corr: Correctness::multi_a(), mall: Malleability::multi_a() } } + /// Constructor for the type of the `sortednmulti_a` fragment. + pub const fn sortedmulti_a() -> Self { + Type { corr: Correctness::sortedmulti_a(), mall: Malleability::sortedmulti_a() } + } + /// Constructor for the type of all the hash fragments. pub const fn hash() -> Self { Type { corr: Correctness::hash(), mall: Malleability::hash() } } @@ -460,6 +465,7 @@ impl Type { Terminal::PkH(..) | Terminal::RawPkH(..) => Ok(Self::pk_h()), Terminal::Multi(..) => Ok(Self::multi()), Terminal::MultiA(..) => Ok(Self::multi_a()), + Terminal::SortedMultiA(..) => Ok(Self::sortedmulti_a()), Terminal::After(_) => Ok(Self::time()), Terminal::Older(_) => Ok(Self::time()), Terminal::Sha256(..) => Ok(Self::hash()), diff --git a/src/policy/compiler.rs b/src/policy/compiler.rs index 4011bf467..018548948 100644 --- a/src/policy/compiler.rs +++ b/src/policy/compiler.rs @@ -224,6 +224,8 @@ impl CompilerExtData { } } + fn sortedmulti_a(k: usize, n: usize) -> Self { Self::multi_a(k, n) } + fn hash() -> Self { CompilerExtData { branch_prob: None, sat_cost: 33.0, dissat_cost: Some(33.0) } } @@ -450,6 +452,7 @@ impl CompilerExtData { Terminal::PkH(..) | Terminal::RawPkH(..) => Self::pk_h::(), Terminal::Multi(ref thresh) => Self::multi(thresh.k(), thresh.n()), Terminal::MultiA(ref thresh) => Self::multi_a(thresh.k(), thresh.n()), + Terminal::SortedMultiA(ref thresh) => Self::sortedmulti_a(thresh.k(), thresh.n()), Terminal::After(_) => Self::time(), Terminal::Older(_) => Self::time(), Terminal::Sha256(..) => Self::hash(), diff --git a/src/policy/mod.rs b/src/policy/mod.rs index 2749167c9..4c769e06f 100644 --- a/src/policy/mod.rs +++ b/src/policy/mod.rs @@ -161,11 +161,13 @@ impl Liftable for Miniscript .map_ref(|key| Arc::new(Semantic::Key(key.clone()))) .forget_maximum(), )), - Terminal::MultiA(ref thresh) => Arc::new(Semantic::Thresh( - thresh - .map_ref(|key| Arc::new(Semantic::Key(key.clone()))) - .forget_maximum(), - )), + Terminal::MultiA(ref thresh) | Terminal::SortedMultiA(ref thresh) => { + Arc::new(Semantic::Thresh( + thresh + .map_ref(|key| Arc::new(Semantic::Key(key.clone()))) + .forget_maximum(), + )) + } }; stack.push(new_term) } diff --git a/src/primitives/threshold.rs b/src/primitives/threshold.rs index f1021e9a4..f132397d4 100644 --- a/src/primitives/threshold.rs +++ b/src/primitives/threshold.rs @@ -10,6 +10,9 @@ use core::{cmp, fmt, iter}; #[cfg(any(feature = "std", test))] use std::vec; +use crate::util::{lex_cmp, lex_cmp_xonly}; +use crate::ToPublicKey; + /// Error parsing an absolute locktime. #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ThresholdError { @@ -220,6 +223,41 @@ impl Threshold { pub fn iter(&self) -> core::slice::Iter<'_, T> { self.inner.iter() } } +impl Threshold { + /// Clones and lexicographically sorts underlying `Vec` using given + /// `cmp` function and returns a new `Threshold`. + fn _to_sorted(&self, cmp: fn(&Pk, &Pk) -> cmp::Ordering) -> Self { + let mut inner = self.inner.clone(); + inner.sort_by(cmp); + Threshold { k: self.k, inner } + } + + /// Clones and lexicographically sorts underlying `Vec` of pubkeys as + /// defined by BIP-67 and returns a new `Threshold`. + pub fn to_sorted(&self) -> Self { Self::_to_sorted(self, lex_cmp) } + + /// Clones and lexicographically sorts underlying `Vec` of x-only pubkeys as + /// defined by BIP-67 and returns a new `Threshold`. + pub fn to_sorted_xonly(&self) -> Self { Self::_to_sorted(self, lex_cmp_xonly) } + + /// Checks if underlying `Vec` of pubkeys is sorted lexicographically from + /// smallest to largest using given `cmp` function. + fn _is_sorted(&self, cmp: fn(&Pk, &Pk) -> cmp::Ordering) -> bool { + // is_sorted_by is not stabalized in the MSRV + self.inner + .windows(2) + .all(|w| matches!(cmp(&w[0], &w[1]), cmp::Ordering::Less | cmp::Ordering::Equal)) + } + + /// Checks if underlying `Vec` of pubkeys is sorted lexicographically from + /// smallest to largest + pub fn is_sorted(&self) -> bool { Self::_is_sorted(self, lex_cmp) } + + /// Checks if underlying `Vec` of x-only pubkeys is sorted lexicographically from + /// smallest to largest + pub fn is_sorted_xonly(&self) -> bool { Self::_is_sorted(self, lex_cmp_xonly) } +} + impl Threshold { /// Constructor for an "or" represented as a 1-of-n threshold. /// diff --git a/src/util.rs b/src/util.rs index 5de37b780..0b95b7436 100644 --- a/src/util.rs +++ b/src/util.rs @@ -109,3 +109,20 @@ impl MsKeyBuilder for script::Builder { } } } + +/// Lexicographically compare pubkeys by bytes as defined in BIP 67 +pub fn lex_cmp(a: &Pk, b: &Pk) -> core::cmp::Ordering { + a.to_public_key() + .inner + .serialize() + .partial_cmp(&b.to_public_key().inner.serialize()) + .unwrap() +} + +/// Lexicographically compare x-only pubkeys by bytes as defined in BIP 67 +pub fn lex_cmp_xonly(a: &Pk, b: &Pk) -> core::cmp::Ordering { + a.to_x_only_pubkey() + .serialize() + .partial_cmp(&b.to_x_only_pubkey().serialize()) + .unwrap() +}