Support musig in multi_a key expressions
What changed, and why it matters
This commit adds support for using MuSig multi-signature key groups inside taproot multi_a wallet policies on Ledger devices, while deliberately blocking them in sortedmulti_a. It also fixes a subtle parsing bug: previously, key expressions inside multi_a/sortedmulti_a were allocated one-by-one in a memory buffer, which would interleave badly with MuSig's own allocations and corrupt the in-memory layout. The new code parses all the keys into a temporary stack array first, then copies them into one contiguous allocation. Separately, the 'cleartext' human-readable display module now refuses to classify multisig patterns that contain musig keys, so the device falls back to showing the raw descriptor instead of potentially misleading text (for example, showing 'any 1 of two keys' for a musig that actually requires both).
Treat as a hardening/feature patch rather than an active vulnerability. Reviewers should verify that parse_multisig_keys() correctly bounds MAX_PUBKEYS_PER_MULTISIG, that the stack-local keys[] array size is safe on the target device's limited stack, and that all_keyexprs_plain() is applied consistently across every generated and hand-written multisig pattern so no musig-containing multisig is misclassified in the UI.
Security signals we found
Memory-layout correctness fix in policy parser (interleaved allocations could corrupt parsed policy nodes)
New guard prevents cleartext UI from misrepresenting musig-in-multisig policies as simpler plain-key multisigs
Feature gate: musig deliberately rejected in sortedmulti_a to avoid expensive/unsafe key re-sorting
Static assertion removed because contiguous allocation is no longer assumed
Test coverage added for both valid and invalid musig-in-multi_a cases
Evidence from the diff
The patch extends descriptor parsing so that multi_a (but not sortedmulti_a) accepts KEY_EXPRESSION_MUSIG entries among its key list. A new parse_multisig_keys() helper parses key expressions into a stack-local keys[] array and performs a single buffer_alloc() + memcpy(), avoiding the previous assumption that consecutive buffer_alloc(sizeof(policy_node_keyexpr_t)) calls would yield a contiguous array. That assumption was broken once musig() key expressions allocate satellite structures during parsing. The cleartext_match classifier is guarded with all_keyexprs_plain() in every multi()/sortedmulti()/multi_a()/sortedmulti_a() pattern so that musig-containing multisigs are not matched and render as ‘(unknown)’. Documentation is updated to state the 5-key musig limit and the sortedmulti_a restriction; tests are added for valid multi_a musig policies and invalid sortedmulti_a musig policies.
Changed components
src/common/wallet.c (policy descriptor parser)src/common/cleartext_match.c (cleartext classifier)src/common/cleartext_match.h (all_keyexprs_plain helper)specs/bip388/gen.py (code generator for classifier)doc/musig.md (documentation)tests/test_e2e_musig2.py (end-to-end tests)unit-tests/test_cleartext.c (cleartext unit tests)unit-tests/test_wallet.c (wallet parser unit tests)Inspect captured patch +219 / −49
### doc/musig.md
@@ -8,7 +8,8 @@ MuSig2 is a 2-round multi-signature scheme compatible with the public keys and s
`musig()` key expressions are supported for all taproot policies, including taproot keypaths and miniscript.
-- At most 16 keys are allowed in the musig expression; performance limitations, however, might apply in practice.
+- At most 5 keys are allowed in the musig expression; performance limitations, however, might apply in practice.
+- `musig(...)` is allowed among the key expressions of `multi_a`, but not of `sortedmulti_a`.
- At most 8 parallel MuSig signing sessions are supported, due to the need to persist state in the device's memory.
- Only `musig(...)/**` or `musig(...)/<M;N>/*` key expressions are supported; the public keys must be xpubs aggregated without any further derivation. Schemes where each pubkey is derived prior to aggregation (for example descriptors similar to `musig(xpub1/<0;1>/*,xpub2/<0;1>/*,...)`) are not supported.
### specs/bip388/gen.py
@@ -487,6 +487,8 @@ def _handle_arg(ctx: _Ctx, arg: Any, ak: str, struct: str, t: str, i: int) -> No
raise ValueError(f"unexpected arg {arg!r} in KeyList position")
ka = ctx.fresh("ka")
ctx.body.append(f"const policy_node_keyexpr_t *{ka} = {t}->keys;")
+ # musig() is not supported in the key list in clear text; leave unclassified.
+ ctx.body.append(f"if (!all_keyexprs_plain({ka}, {t}->n)) break;")
slot = ctx.bidx[arg[1]]
ctx.binds.append(f"set_binding_keys(&out->bindings, {slot}, {ka}, {t}->n);")
elif ak == "SUB":
### src/common/cleartext_match.c
@@ -57,6 +57,7 @@ bool match_top_level(const policy_node_t *root, ct_top_match_t *out) {
if (ct_c2 == NULL || ct_c2->type != TOKEN_MULTI) break;
const policy_node_multisig_t *ct_n3 = (const policy_node_multisig_t *) ct_c2;
const policy_node_keyexpr_t *ct_ka4 = ct_n3->keys;
+ if (!all_keyexprs_plain(ct_ka4, ct_n3->n)) break;
set_binding_number(&out->bindings, 0, ct_n3->k);
set_binding_keys(&out->bindings, 1, ct_ka4, ct_n3->n);
out->bindings.n = 2;
@@ -71,6 +72,7 @@ bool match_top_level(const policy_node_t *root, ct_top_match_t *out) {
if (ct_c2 == NULL || ct_c2->type != TOKEN_SORTEDMULTI) break;
const policy_node_multisig_t *ct_n3 = (const policy_node_multisig_t *) ct_c2;
const policy_node_keyexpr_t *ct_ka4 = ct_n3->keys;
+ if (!all_keyexprs_plain(ct_ka4, ct_n3->n)) break;
set_binding_number(&out->bindings, 0, ct_n3->k);
set_binding_keys(&out->bindings, 1, ct_ka4, ct_n3->n);
out->bindings.n = 2;
@@ -85,6 +87,7 @@ bool match_top_level(const policy_node_t *root, ct_top_match_t *out) {
if (ct_c2 == NULL || ct_c2->type != TOKEN_MULTI) break;
const policy_node_multisig_t *ct_n3 = (const policy_node_multisig_t *) ct_c2;
const policy_node_keyexpr_t *ct_ka4 = ct_n3->keys;
+ if (!all_keyexprs_plain(ct_ka4, ct_n3->n)) break;
set_binding_number(&out->bindings, 0, ct_n3->k);
set_binding_keys(&out->bindings, 1, ct_ka4, ct_n3->n);
out->bindings.n = 2;
@@ -99,6 +102,7 @@ bool match_top_level(const policy_node_t *root, ct_top_match_t *out) {
if (ct_c2 == NULL || ct_c2->type != TOKEN_SORTEDMULTI) break;
const policy_node_multisig_t *ct_n3 = (const policy_node_multisig_t *) ct_c2;
const policy_node_keyexpr_t *ct_ka4 = ct_n3->keys;
+ if (!all_keyexprs_plain(ct_ka4, ct_n3->n)) break;
set_binding_number(&out->bindings, 0, ct_n3->k);
set_binding_keys(&out->bindings, 1, ct_ka4, ct_n3->n);
out->bindings.n = 2;
@@ -116,6 +120,7 @@ bool match_top_level(const policy_node_t *root, ct_top_match_t *out) {
if (ct_c4 == NULL || ct_c4->type != TOKEN_MULTI) break;
const policy_node_multisig_t *ct_n5 = (const policy_node_multisig_t *) ct_c4;
const policy_node_keyexpr_t *ct_ka6 = ct_n5->keys;
+ if (!all_keyexprs_plain(ct_ka6, ct_n5->n)) break;
set_binding_number(&out->bindings, 0, ct_n5->k);
set_binding_keys(&out->bindings, 1, ct_ka6, ct_n5->n);
out->bindings.n = 2;
@@ -133,6 +138,7 @@ bool match_top_level(const policy_node_t *root, ct_top_match_t *out) {
if (ct_c4 == NULL || ct_c4->type != TOKEN_SORTEDMULTI) break;
const policy_node_multisig_t *ct_n5 = (const policy_node_multisig_t *) ct_c4;
const policy_node_keyexpr_t *ct_ka6 = ct_n5->keys;
+ if (!all_keyexprs_plain(ct_ka6, ct_n5->n)) break;
set_binding_number(&out->bindings, 0, ct_n5->k);
set_binding_keys(&out->bindings, 1, ct_ka6, ct_n5->n);
out->bindings.n = 2;
@@ -249,6 +255,7 @@ bool match_tapleaf(const policy_node_t *leaf_script, ct_leaf_match_t *out) {
if (leaf_script == NULL || leaf_script->type != TOKEN_SORTEDMULTI_A) break;
const policy_node_multisig_t *ct_n1 = (const policy_node_multisig_t *) leaf_script;
const policy_node_keyexpr_t *ct_ka2 = ct_n1->keys;
+ if (!all_keyexprs_plain(ct_ka2, ct_n1->n)) break;
set_binding_number(&out->bindings, 0, ct_n1->k);
set_binding_keys(&out->bindings, 1, ct_ka2, ct_n1->n);
out->bindings.n = 2;
@@ -261,6 +268,7 @@ bool match_tapleaf(const policy_node_t *leaf_script, ct_leaf_match_t *out) {
if (leaf_script == NULL || leaf_script->type != TOKEN_MULTI_A) break;
const policy_node_multisig_t *ct_n1 = (const policy_node_multisig_t *) leaf_script;
const policy_node_keyexpr_t *ct_ka2 = ct_n1->keys;
+ if (!all_keyexprs_plain(ct_ka2, ct_n1->n)) break;
set_binding_number(&out->bindings, 0, ct_n1->k);
set_binding_keys(&out->bindings, 1, ct_ka2, ct_n1->n);
out->bindings.n = 2;
### src/common/cleartext_match.h
@@ -70,9 +70,17 @@ typedef struct {
} ct_leaf_match_t;
// ---------------------------------------------------------------------------
-// Binding setters used by the generated classifier.
+// Guards and binding setters used by the generated classifier.
// ---------------------------------------------------------------------------
+// Returns true iff all the `n` key expressions in `keys` are plain (non-musig) ones.
+static inline bool all_keyexprs_plain(const policy_node_keyexpr_t *keys, uint16_t n) {
+ for (uint16_t i = 0; i < n; i++) {
+ if (keys[i].type != KEY_EXPRESSION_NORMAL) return false;
+ }
+ return true;
+}
+
static inline void set_binding_key(ct_bindings_t *b, int i, const policy_node_keyexpr_t *k) {
b->v[i].kind = CT_BV_KEY;
b->v[i].u.key = k;
### src/common/wallet.c
@@ -18,12 +18,6 @@
#include "../crypto.h"
-// The key expressions of multi() and friends are allocated one by one, but then accessed as an
-// array through policy_node_multisig_t::keys; that only works if buffer_alloc() lays them out
-// back-to-back, that is, if their size is a multiple of the alignment it pads to.
-_Static_assert(sizeof(policy_node_keyexpr_t) % 4 == 0,
- "policy_node_keyexpr_t must be a multiple of the buffer_alloc() alignment");
-
typedef struct {
PolicyNodeType type;
const char *name;
@@ -470,7 +464,7 @@ static int parse_keyexpr(buffer_t *in_buf,
}
if (!allow_musig) {
- return WITH_ERROR(-1, "musig is only allowed in taproot");
+ return WITH_ERROR(-1, "musig is not allowed in this key expression");
}
out->type = KEY_EXPRESSION_MUSIG;
@@ -600,6 +594,62 @@ static int parse_keyexpr(buffer_t *in_buf,
return 0;
}
+/**
+ * Parses the comma-separated key expressions of a multi() family fragment, up to (but not
+ * consuming) the ')' that closes it, filling in `node->keys` and `node->n`.
+ *
+ * The key expressions are parsed into a temporary array, then copied into a single allocation:
+ * `node->keys` is used as an array, but a musig() key expression allocates its own satellite
+ * structures from `out_buf` while it is being parsed, so allocating the key expressions one at a
+ * time would interleave them with those and break the layout.
+ *
+ * The `noinline` is required: inlined in parse_script(), the temporary array would live in that
+ * function's frame, which would increase the stack usage as it is recursive.
+ */
+__attribute__((noinline)) static int parse_multisig_keys(buffer_t *in_buf,
+ int version,
+ policy_node_multisig_t *node,
+ bool allow_musig,
+ buffer_t *out_buf,
+ uint16_t *keyexpr_index) {
+ policy_node_keyexpr_t keys[MAX_PUBKEYS_PER_MULTISIG];
+ uint16_t n = 0;
+
+ while (true) {
+ uint8_t c;
+ // If the next character is a ')', we exit and leave it in the buffer
+ if (buffer_peek(in_buf, &c) && c == ')') {
+ break;
+ }
+
+ // otherwise, there must be a comma
+ if (!consume_character(in_buf, ',')) {
+ return WITH_ERROR(-1, "Expected ','");
+ }
+
+ if (n >= MAX_PUBKEYS_PER_MULTISIG) {
+ return WITH_ERROR(-1, "Too many key expressions");
+ }
+
+ if (0 > parse_keyexpr(in_buf, version, &keys[n], allow_musig, out_buf, keyexpr_index)) {
+ return WITH_ERROR(-1, "Error parsing key expression");
+ }
+
+ ++n;
+ }
+
+ node->keys = (policy_node_keyexpr_t *) buffer_alloc(out_buf,
+ (size_t) n * sizeof(policy_node_keyexpr_t),
+ true);
+ if (node->keys == NULL) {
+ return WITH_ERROR(-1, "Out of memory");
+ }
+ memcpy(node->keys, keys, (size_t) n * sizeof(policy_node_keyexpr_t));
+ node->n = n;
+
+ return 0;
+}
+
#define CONTEXT_WITHIN_SH 1 // parsing a direct child of SH
#define CONTEXT_WITHIN_WSH 2 // parsing a direct child of WSH
#define CONTEXT_WITHIN_TR 4 // parsing a child of TR (direct or not)
@@ -1729,47 +1779,16 @@ static int parse_script(buffer_t *in_buf,
}
node->k = (int16_t) k;
- // We allocate the array of key indices at the current position in the output buffer
- // (on success).
- // Note: this is incompatible with musig keys, therefore we don't currently support
- // musig nested inside multi_a or sortedmulti_a.
- buffer_alloc(out_buf, 0, true); // ensure alignment of current pointer
- node->keys = (policy_node_keyexpr_t *) buffer_get_cur(out_buf);
-
- node->n = 0;
- while (true) {
- uint8_t c;
- // If the next character is a ')', we exit and leave it in the buffer
- if (buffer_peek(in_buf, &c) && c == ')') {
- break;
- }
-
- // otherwise, there must be a comma
- if (!consume_character(in_buf, ',')) {
- return WITH_ERROR(-1, "Expected ','");
- }
-
- policy_node_keyexpr_t *key_expr = (policy_node_keyexpr_t *) buffer_alloc(
- out_buf,
- sizeof(policy_node_keyexpr_t),
- true); // we align this pointer, as there's padding in an array of
- // structures
- if (key_expr == NULL) {
- return WITH_ERROR(-1, "Out of memory");
- }
-
- if (0 >
- parse_keyexpr(
+ if (0 > parse_multisig_keys(
in_buf,
version,
- key_expr,
- false, // musig is not currently supported in keys of multisig fragments
+ node,
+ // musig is only supported in multi_a; we do not support it in
+ // sortedmulti_a, where sorting the keys would become complicated
+ token == TOKEN_MULTI_A,
out_buf,
&key_expression_count)) {
- return WITH_ERROR(-1, "Error parsing key expression");
- }
-
- ++node->n;
+ return -1;
}
// check integrity of k and n
### tests/test_e2e_musig2.py
@@ -301,6 +301,32 @@ def test_e2e_musig2_scriptpath(navigator: Navigator, firmware: Firmware, client:
e2e_register_wallet_instruction(firmware, wallet_policy.n_keys), e2e_sign_psbt_instruction(firmware), test_name)
+def test_e2e_musig2_multi_a_scriptpath(navigator: Navigator, firmware: Firmware, client: RaggerClient,
+ test_name: str, rpc, rpc_test_wallet, speculos_globals: SpeculosGlobals):
+ path = "48'/1'/0'/2'"
+ internal_xpub = get_internal_xpub(speculos_globals.seed, path)
+
+ core_wallet_name_1, core_xpub_orig_1 = create_new_wallet()
+ core_wallet_name_2, core_xpub_orig_2 = create_new_wallet()
+
+ # In this policy, the keypath is unspendable; the only spending path is a 2-of-2 multi_a where
+ # one of the two keys is a musig() of the device key and the first cosigner
+ # (so, in practice, all keys must sign)
+
+ wallet_policy = WalletPolicy(
+ name="Musig in multi_a",
+ descriptor_template="tr(@0/**,multi_a(2,musig(@1,@2)/**,@3/**))",
+ keys_info=[
+ "tpubD6NzVbkrYhZ4WLczPJWReQycCJdd6YVWXubbVUFnJ5KgU5MDQrD998ZJLSmaB7GVcCnJSDWprxmrGkJ6SvgQC6QAffVpqSvonXmeizXcrkN",
+ f"[{speculos_globals.master_key_fingerprint.hex()}/{path}]{internal_xpub}",
+ f"{core_xpub_orig_1}",
+ f"{core_xpub_orig_2}",
+ ])
+
+ run_test_e2e_musig2(navigator, client, wallet_policy, [core_wallet_name_1, core_wallet_name_2], rpc, rpc_test_wallet, speculos_globals,
+ e2e_register_wallet_instruction(firmware, wallet_policy.n_keys), e2e_sign_psbt_instruction(firmware), test_name)
+
+
def test_e2e_musig2_3of3keypath_decaying_scriptpath(navigator: Navigator, firmware: Firmware, client: RaggerClient,
test_name: str, rpc, rpc_test_wallet, speculos_globals: SpeculosGlobals):
path = "48'/1'/0'/2'"
@@ -365,6 +391,8 @@ def test_e2e_musig2_5of5(navigator: Navigator, firmware: Firmware, client: Ragge
def test_e2e_musig_invalid(client: RaggerClient, speculos_globals: SpeculosGlobals):
path = "48'/1'/0'/2'"
text_xpub_1 = "tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT"
+ text_xpub_2 = "tpubD6NzVbkrYhZ4WLczPJWReQycCJdd6YVWXubbVUFnJ5KgU5MDQrD998ZJLSmaB7GVcCnJSDWprxmrGkJ6SvgQC6QAffVpqSvonXmeizXcrkN"
+ text_xpub_3 = "tpubD6NzVbkrYhZ4YAPXpMw61GrdqXJJEiYhHo6wxVkfwZgUged5qXm6Df4NLf8ZTFXxW1UhxDKGeKdAVxZtmodC8KfR7SqmW6LGQfDGfnFLmQ6"
internal_xpub = get_internal_xpub(speculos_globals.seed, path)
internal_xpub_orig = f"[{speculos_globals.master_key_fingerprint.hex()}/{path}]{internal_xpub}"
@@ -398,3 +426,8 @@ def test_e2e_musig_invalid(client: RaggerClient, speculos_globals: SpeculosGloba
# supported in BIP-390, not in BIP-388
run_test_invalid(client, "tr(musig(@0/**,@1/**))", two_keys)
+
+ # musig() is supported in multi_a, but we do not support it in sortedmulti_a.
+ # The same policy with multi_a instead is valid, and covered by test_e2e_musig2_multi_a_scriptpath.
+ run_test_invalid(client, "tr(@0/**,sortedmulti_a(2,musig(@1,@2)/**,@3/**))",
+ [text_xpub_1, internal_xpub_orig, text_xpub_2, text_xpub_3])
### unit-tests/test_cleartext.c
@@ -254,12 +254,36 @@ static void test_ct_too_many_leaves(void **state) {
expect_encode(big, 1, false, NULL, 0);
}
+// A musig() among the keys of a multi_a is not classified: the `$keys` binding of the multisig
+// patterns only accepts plain key expressions, since a single-element list holding a musig is how
+// pk(musig(...)) is bound. Such a leaf therefore renders as the "(unknown)" marker, and the app
+// falls back to showing the raw descriptor template.
+// This behaviour is not in the shared vectors of specs/bip388/test_vectors.toml yet, as the
+// reference implementation has to grow the same guard first.
+static void test_ct_musig_in_multi_a_is_not_classified(void **state) {
+ (void) state;
+ static const char *const want[] = {"Main path: spendable by @0", "(unknown)"};
+ expect_encode("tr(@0/**,multi_a(2,musig(@1,@2)/**,@3/**))", 1, false, want, 2);
+
+ // The 1-key case is the sharp one: a single-element key list holding a musig is precisely the
+ // sentinel for pk(musig(...)), whose keys are rendered flattened. Without the guard, this leaf
+ // would therefore read "any 1 of @1 and @2 must sign", while a musig aggregate key requires
+ // *both* of them to sign.
+ expect_encode("tr(@0/**,multi_a(1,musig(@1,@2)/**))", 1, false, want, 2);
+
+ // for comparison: the same leaf with plain key expressions does classify
+ static const char *const want_plain[] = {"Main path: spendable by @0",
+ "Any 2 of @1, @2 and @3 must sign"};
+ expect_encode("tr(@0/**,multi_a(2,@1/**,@2/**,@3/**))", 1, true, want_plain, 2);
+}
+
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_ct_confusion_score),
cmocka_unit_test(test_ct_to_cleartext),
cmocka_unit_test(test_ct_line_overflow),
cmocka_unit_test(test_ct_too_many_leaves),
+ cmocka_unit_test(test_ct_musig_in_multi_a_is_not_classified),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
### unit-tests/test_wallet.c
@@ -314,6 +314,66 @@ static void test_parse_policy_tr_musig_scriptpath(void **state) {
check_key_expr_musig(script_pk->key, 3, (uint16_t[]) {2, 0, 3}, 0, 1);
}
+static void test_parse_policy_tr_multi_a_musig(void **state) {
+ (void) state;
+
+ uint8_t out[MAX_WALLET_POLICY_BYTES];
+ int res;
+
+ // tr with a musig among the keys of a multi_a in the script path
+ res = parse_policy("tr(@0/**,multi_a(2,musig(@1,@2)/**,@3/**))", out, sizeof(out));
+
+ assert_true(res >= 0);
+
+ policy_node_tr_t *root = (policy_node_tr_t *) out;
+ assert_int_equal(root->base.type, TOKEN_TR);
+ check_key_expr_plain(root->key, 0, 0, 1);
+
+ assert_false(root->tree == NULL);
+ policy_node_tree_t *tree = root->tree;
+ assert_true(tree->is_leaf);
+
+ policy_node_multisig_t *multi_a = (policy_node_multisig_t *) tree->script;
+ assert_int_equal(multi_a->base.type, TOKEN_MULTI_A);
+ assert_int_equal(multi_a->k, 2);
+ assert_int_equal(multi_a->n, 2);
+
+ check_key_expr_musig(&multi_a->keys[0], 2, (uint16_t[]) {1, 2}, 0, 1);
+ check_key_expr_plain(&multi_a->keys[1], 3, 0, 1);
+}
+
+static void test_parse_policy_tr_multi_a_two_musigs(void **state) {
+ (void) state;
+
+ uint8_t out[MAX_WALLET_POLICY_BYTES];
+ int res;
+
+ // a multi_a with a plain key expression followed by two musig ones; each musig allocates its
+ // own structures while being parsed, so this checks that the key expressions are still laid
+ // out correctly in memory
+ res = parse_policy("tr(@0/**,multi_a(2,@1/**,musig(@2,@3)/<2;3>/*,musig(@4,@5,@6)/**))",
+ out,
+ sizeof(out));
+
+ assert_true(res >= 0);
+
+ policy_node_tr_t *root = (policy_node_tr_t *) out;
+ assert_int_equal(root->base.type, TOKEN_TR);
+
+ assert_false(root->tree == NULL);
+ policy_node_tree_t *tree = root->tree;
+ assert_true(tree->is_leaf);
+
+ policy_node_multisig_t *multi_a = (policy_node_multisig_t *) tree->script;
+ assert_int_equal(multi_a->base.type, TOKEN_MULTI_A);
+ assert_int_equal(multi_a->k, 2);
+ assert_int_equal(multi_a->n, 3);
+
+ check_key_expr_plain(&multi_a->keys[0], 1, 0, 1);
+ check_key_expr_musig(&multi_a->keys[1], 2, (uint16_t[]) {2, 3}, 2, 3);
+ check_key_expr_musig(&multi_a->keys[2], 3, (uint16_t[]) {4, 5, 6}, 0, 1);
+}
+
static void test_get_policy_segwit_version(void **state) {
(void) state;
@@ -445,11 +505,24 @@ static void test_failures(void **state) {
assert_true(
0 > parse_policy("tr(musig(@0,musig(@1,@2))/**)", out, sizeof(out))); // can't nest musig
- // musig is currently disabled in multi_a/sortedmulti_a, until the parsing
- // of such expressions is properly fixed in parse_policy
- assert_true(0 > parse_policy("tr(@0/**,multi_a(1,musig(@1,@2)/**))", out, sizeof(out)));
+ assert_true(0 > parse_policy("wsh(multi(2,musig(@0,@1)/**,@2/**))",
+ out,
+ sizeof(out))); // not taproot
+ assert_true(0 > parse_policy("tr(@0/**,multi_a(2,musig(@1,musig(@2,@3))/**,@4/**))",
+ out,
+ sizeof(out))); // can't nest musig
+
+ // musig is only supported in multi_a, and deliberately rejected in sortedmulti_a: sorting the
+ // keys would require recomputing each aggregate key once per position
assert_true(0 >
parse_policy("tr(@0/**,sortedmulti_a(2,musig(@1,@2)/**,@3/**))", out, sizeof(out)));
+
+ // at most MAX_PUBKEYS_PER_MULTISIG (16) key expressions; this one has 17
+ assert_true(0 > parse_policy("tr(@0/**,multi_a(2,@1/**,@2/**,@3/**,@4/**,@5/**,@6/**,@7/**,"
+ "@8/**,@9/**,@10/**,@11/**,@12/**,@13/**,@14/**,@15/**,@16/**,"
+ "@17/**))",
+ out,
+ sizeof(out)));
}
enum TestMode {
@@ -1040,6 +1113,8 @@ int main() {
cmocka_unit_test(test_parse_policy_tr_multisig),
cmocka_unit_test(test_parse_policy_tr_musig_scriptpath),
cmocka_unit_test(test_parse_policy_tr_musig_keypath),
+ cmocka_unit_test(test_parse_policy_tr_multi_a_musig),
+ cmocka_unit_test(test_parse_policy_tr_multi_a_two_musigs),
cmocka_unit_test(test_get_policy_segwit_version),
cmocka_unit_test(test_parse_unsigned_decimal_overflow),
cmocka_unit_test(test_parse_keyexpr_multipath_hardened_boundary),Why this scored 29/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.