diff --git a/src/modules/prefractal/tests_impl.h b/src/modules/prefractal/tests_impl.h index 7a8026d8..8fb82b38 100644 --- a/src/modules/prefractal/tests_impl.h +++ b/src/modules/prefractal/tests_impl.h @@ -1,14 +1,544 @@ +/*********************************************************************** + * Distributed under the MIT software license, see the accompanying * + * file COPYING or https://www.opensource.org/licenses/mit-license.php.* + ***********************************************************************/ + #ifndef SECP256K1_MODULE_PREFRACTAL_TESTS_IMPL_H #define SECP256K1_MODULE_PREFRACTAL_TESTS_IMPL_H #include "../../../include/secp256k1_prefractal.h" -static void run_prefractal_smoke_test(void) { - CHECK(1); +/* Two FIXED threshold secret keys, one of each Y parity of the resulting + * threshold public key. + * + * The odd one is the point of this pair. The nested equation has no g_frost + * factor, and the reason is NOT that the tweak cache is the identity: stock + * frost's key-side factor is g*gacc with g = -1 for an odd-Y threshold key, so + * an implementation that imported frost's key-side parity would work for + * even-Y groups and fail for odd-Y ones. A randomly seeded fixture makes that + * a coin flip per run, so both parities are pinned here instead. */ +static const unsigned char prefractal_test_seckey_even[32] = { + 0x44, 0xa2, 0x82, 0x5e, 0x46, 0x26, 0xfa, 0x53, + 0xf5, 0x2c, 0x2e, 0x6a, 0x40, 0x7a, 0xfc, 0xb9, + 0xb7, 0xe8, 0x7d, 0x63, 0x30, 0x6b, 0x0d, 0x69, + 0xae, 0x3d, 0x0d, 0x29, 0xeb, 0x6c, 0xa6, 0x08 +}; +static const unsigned char prefractal_test_seckey_odd[32] = { + 0xd3, 0x27, 0x59, 0x3f, 0xe7, 0x53, 0xf6, 0xfd, + 0xe3, 0x8f, 0x29, 0xfd, 0x26, 0x39, 0xd4, 0x4f, + 0x62, 0x05, 0x4b, 0xab, 0xea, 0x21, 0xa3, 0x59, + 0xa4, 0x1d, 0x65, 0x1c, 0x81, 0xf1, 0xe0, 0x1e +}; + +/* Everything one nested session needs, so the tests below can set one up in a + * line and then poke at individual pieces. */ +typedef struct { + unsigned int n, t; + unsigned char secshares[SECP256K1_FROST_MAX_PARTICIPANTS][32]; + secp256k1_pubkey pubshares[SECP256K1_FROST_MAX_PARTICIPANTS]; + secp256k1_pubkey thresh_pk; + secp256k1_frost_tweak_cache tweak_cache; + uint32_t ids[SECP256K1_FROST_MAX_PARTICIPANTS]; + + secp256k1_frost_secnonce secnonces[SECP256K1_FROST_MAX_PARTICIPANTS]; + secp256k1_frost_pubnonce pubnonces[SECP256K1_FROST_MAX_PARTICIPANTS]; + const secp256k1_frost_pubnonce *pubnonce_ptrs[SECP256K1_FROST_MAX_PARTICIPANTS]; + secp256k1_frost_aggnonce aggnonce; + secp256k1_musig_pubnonce group_pubnonce; + + secp256k1_keypair cosigner_keypair; + secp256k1_pubkey cosigner_pk; + secp256k1_xonly_pubkey agg_xonly; + secp256k1_musig_keyagg_cache keyagg_cache; + secp256k1_musig_secnonce cosigner_secnonce; + secp256k1_musig_pubnonce cosigner_pubnonce; + secp256k1_musig_aggnonce cosigner_aggnonce; + secp256k1_musig_aggnonce full_aggnonce; + + unsigned char msg[32]; +} prefractal_test_session; + +/* Deal a group, build the outer 2-key aggregation with one stock musig + * cosigner, and run round one on both sides. + * + * group_first picks which side of the outer aggregation the group key sits on. + * BIP 327 KeyAgg gives the "second" distinct key a coefficient of exactly 1 + * and hashes every other one, so the two orders exercise genuinely different + * arithmetic for the group; both are run everywhere below. + * + * xonly_tweak applies the BIP 341 style tweak to the OUTER cache only, which + * is the arrangement the channel protocols use. */ +static void prefractal_test_setup(prefractal_test_session *s, unsigned int n, unsigned int t, + const unsigned char *thresh_seckey, int group_first, + const unsigned char *xonly_tweak) { + const secp256k1_pubkey *pubkeys[2]; + const secp256k1_musig_pubnonce *just_cosigner[1]; + unsigned char cosigner_seckey[32], secrand[32], thresh_pk32[32]; + secp256k1_xonly_pubkey thresh_xonly; + unsigned int k; + + s->n = n; + s->t = t; + testrand256(s->msg); + testrand256(cosigner_seckey); + + CHECK(secp256k1_frost_trusted_dealer_keygen(CTX, s->secshares[0], &s->thresh_pk, + s->pubshares, n, t, thresh_seckey) == 1); + CHECK(secp256k1_frost_tweak_cache_init(CTX, &s->tweak_cache, &s->thresh_pk) == 1); + for (k = 0; k < n; k++) { + s->ids[k] = k; + } + + /* The group's key enters the outer aggregation as an ordinary public key, + * in full. */ + CHECK(secp256k1_keypair_create(CTX, &s->cosigner_keypair, cosigner_seckey) == 1); + CHECK(secp256k1_keypair_pub(CTX, &s->cosigner_pk, &s->cosigner_keypair) == 1); + pubkeys[group_first ? 0 : 1] = &s->thresh_pk; + pubkeys[group_first ? 1 : 0] = &s->cosigner_pk; + CHECK(secp256k1_musig_pubkey_agg(CTX, &s->agg_xonly, &s->keyagg_cache, pubkeys, 2) == 1); + if (xonly_tweak != NULL) { + secp256k1_pubkey tweaked; + CHECK(secp256k1_musig_pubkey_xonly_tweak_add(CTX, &tweaked, &s->keyagg_cache, xonly_tweak) == 1); + CHECK(secp256k1_xonly_pubkey_from_pubkey(CTX, &s->agg_xonly, NULL, &tweaked) == 1); + } + + /* Round one, cosigner side. */ + testrand256(secrand); + CHECK(secp256k1_musig_nonce_gen(CTX, &s->cosigner_secnonce, &s->cosigner_pubnonce, secrand, + cosigner_seckey, &s->cosigner_pk, s->msg, &s->keyagg_cache, NULL) == 1); + just_cosigner[0] = &s->cosigner_pubnonce; + CHECK(secp256k1_musig_nonce_agg(CTX, &s->cosigner_aggnonce, just_cosigner, 1) == 1); + + /* Round one, group side. The nonces are generated the way the module + * expects them: msg = NULL, because the wire nonce is published before the + * message exists. */ + CHECK(secp256k1_xonly_pubkey_from_pubkey(CTX, &thresh_xonly, NULL, &s->thresh_pk) == 1); + CHECK(secp256k1_xonly_pubkey_serialize(CTX, thresh_pk32, &thresh_xonly) == 1); + for (k = 0; k < t; k++) { + testrand256(secrand); + CHECK(secp256k1_frost_nonce_gen(CTX, &s->secnonces[k], &s->pubnonces[k], secrand, + s->secshares[k], &s->pubshares[k], thresh_pk32, + NULL, 0, NULL, 0) == 1); + s->pubnonce_ptrs[k] = &s->pubnonces[k]; + } + CHECK(secp256k1_prefractal_nonce_agg(CTX, &s->group_pubnonce, &s->aggnonce, + s->pubnonce_ptrs, s->ids, t, &s->thresh_pk) == 1); + + { + const secp256k1_musig_pubnonce *all[2]; + all[group_first ? 0 : 1] = &s->group_pubnonce; + all[group_first ? 1 : 0] = &s->cosigner_pubnonce; + CHECK(secp256k1_musig_nonce_agg(CTX, &s->full_aggnonce, all, 2) == 1); + } +} + +/* Round two on both sides, ending in a BIP 340 signature over the outer + * aggregate key. Returns what secp256k1_schnorrsig_verify says about it. */ +static int prefractal_test_finish(prefractal_test_session *s, int group_first, unsigned char *sig64) { + secp256k1_frost_partial_sig psigs[SECP256K1_FROST_MAX_PARTICIPANTS]; + const secp256k1_frost_partial_sig *psig_ptrs[SECP256K1_FROST_MAX_PARTICIPANTS]; + const secp256k1_musig_partial_sig *musig_psigs[2]; + secp256k1_musig_partial_sig group_psig, cosigner_psig; + secp256k1_musig_session session; + unsigned int k; + + CHECK(secp256k1_musig_nonce_process(CTX, &session, &s->full_aggnonce, s->msg, &s->keyagg_cache, NULL) == 1); + CHECK(secp256k1_musig_partial_sign(CTX, &cosigner_psig, &s->cosigner_secnonce, + &s->cosigner_keypair, &s->keyagg_cache, &session) == 1); + + for (k = 0; k < s->t; k++) { + CHECK(secp256k1_prefractal_sign(CTX, &psigs[k], &s->secnonces[k], s->secshares[k], + s->ids[k], s->ids, s->pubshares, s->t, &s->aggnonce, + &s->thresh_pk, &s->tweak_cache, &s->keyagg_cache, + &s->cosigner_aggnonce, s->msg) == 1); + psig_ptrs[k] = &psigs[k]; + /* Every share verifies against its author's public share. */ + CHECK(secp256k1_prefractal_partial_sig_verify(CTX, &psigs[k], &s->pubnonces[k], + &s->pubshares[k], s->ids[k], s->ids, s->t, + &s->aggnonce, &s->thresh_pk, &s->tweak_cache, + &s->keyagg_cache, &s->cosigner_aggnonce, + s->msg) == 1); + } + CHECK(secp256k1_prefractal_partial_sig_agg(CTX, &group_psig, NULL, psig_ptrs, s->t, + &s->tweak_cache) == 1); + + musig_psigs[group_first ? 0 : 1] = &group_psig; + musig_psigs[group_first ? 1 : 0] = &cosigner_psig; + CHECK(secp256k1_musig_partial_sig_agg(CTX, sig64, &session, musig_psigs, 2) == 1); + return secp256k1_schnorrsig_verify(CTX, sig64, s->msg, 32, &s->agg_xonly); +} + +/* The round trip, over every t-of-n this module claims to support, both + * lexicographic positions of the group key, both group-key Y parities, and + * with and without the outer BIP 341 tweak. */ +static void run_prefractal_e2e_test(void) { + static const unsigned char configs[][2] = { {2,2}, {3,2}, {5,3}, {4,3}, {7,5} }; + size_t config; + int group_first, odd, tweaked; + + for (config = 0; config < sizeof(configs) / sizeof(configs[0]); config++) { + for (group_first = 0; group_first <= 1; group_first++) { + for (odd = 0; odd <= 1; odd++) { + for (tweaked = 0; tweaked <= 1; tweaked++) { + prefractal_test_session s; + unsigned char sig[64], tweak[32]; + testrand256(tweak); + prefractal_test_setup(&s, configs[config][0], configs[config][1], + odd ? prefractal_test_seckey_odd : prefractal_test_seckey_even, + group_first, tweaked ? tweak : NULL); + CHECK(prefractal_test_finish(&s, group_first, sig) == 1); + } + } + } + } +} + +/* The odd-Y case on its own, so a failure names the cause rather than showing + * up as one iteration of the matrix above. If the g_frost factor were imported + * from stock frost, this test would fail and the even-Y one would pass. */ +static void run_prefractal_odd_y_group_key_test(void) { + prefractal_test_session s; + secp256k1_xonly_pubkey xonly; + unsigned char sig[64], ser[32]; + int parity = -1; + + prefractal_test_setup(&s, 3, 2, prefractal_test_seckey_odd, 1, NULL); + /* Confirm the fixture really is odd-Y, so the test cannot quietly stop + * testing what it is named after. */ + CHECK(secp256k1_xonly_pubkey_from_pubkey(CTX, &xonly, &parity, &s.thresh_pk) == 1); + CHECK(secp256k1_xonly_pubkey_serialize(CTX, ser, &xonly) == 1); + CHECK(parity == 1); + CHECK(prefractal_test_finish(&s, 1, sig) == 1); +} + +/* partial_sig_verify is the identifiable-abort tool, so it has to say no to + * everything that is not exactly the share it was asked about. */ +static void run_prefractal_partial_sig_verify_test(void) { + prefractal_test_session s; + secp256k1_frost_partial_sig psig, altered; + uint32_t wrong_ids[SECP256K1_FROST_MAX_PARTICIPANTS]; + unsigned char other_msg[32]; + unsigned int k; + + prefractal_test_setup(&s, 5, 3, prefractal_test_seckey_odd, 1, NULL); + CHECK(secp256k1_prefractal_sign(CTX, &psig, &s.secnonces[0], s.secshares[0], s.ids[0], + s.ids, s.pubshares, s.t, &s.aggnonce, &s.thresh_pk, + &s.tweak_cache, &s.keyagg_cache, &s.cosigner_aggnonce, + s.msg) == 1); + CHECK(secp256k1_prefractal_partial_sig_verify(CTX, &psig, &s.pubnonces[0], &s.pubshares[0], + s.ids[0], s.ids, s.t, &s.aggnonce, &s.thresh_pk, + &s.tweak_cache, &s.keyagg_cache, + &s.cosigner_aggnonce, s.msg) == 1); + + /* A tampered share. */ + altered = psig; + altered.data[10] ^= 0x40; + CHECK(secp256k1_prefractal_partial_sig_verify(CTX, &altered, &s.pubnonces[0], &s.pubshares[0], + s.ids[0], s.ids, s.t, &s.aggnonce, &s.thresh_pk, + &s.tweak_cache, &s.keyagg_cache, + &s.cosigner_aggnonce, s.msg) == 0); + + /* The right share against the wrong member's public share and nonce. */ + CHECK(secp256k1_prefractal_partial_sig_verify(CTX, &psig, &s.pubnonces[1], &s.pubshares[1], + s.ids[1], s.ids, s.t, &s.aggnonce, &s.thresh_pk, + &s.tweak_cache, &s.keyagg_cache, + &s.cosigner_aggnonce, s.msg) == 0); + + /* A different signer set. lambda_i is defined over the participating set, + * so a share made for one set does not verify under another even though + * every other argument is unchanged. */ + for (k = 0; k < s.t; k++) { + wrong_ids[k] = s.ids[k]; + } + wrong_ids[s.t - 1] = s.ids[s.t]; + CHECK(secp256k1_prefractal_partial_sig_verify(CTX, &psig, &s.pubnonces[0], &s.pubshares[0], + s.ids[0], wrong_ids, s.t, &s.aggnonce, + &s.thresh_pk, &s.tweak_cache, &s.keyagg_cache, + &s.cosigner_aggnonce, s.msg) == 0); + + /* A different message gives the same 0 as a bad share, which is why a 0 is + * not by itself evidence about a member. */ + memcpy(other_msg, s.msg, 32); + other_msg[0] ^= 1; + CHECK(secp256k1_prefractal_partial_sig_verify(CTX, &psig, &s.pubnonces[0], &s.pubshares[0], + s.ids[0], s.ids, s.t, &s.aggnonce, &s.thresh_pk, + &s.tweak_cache, &s.keyagg_cache, + &s.cosigner_aggnonce, other_msg) == 0); +} + +/* A non-identity frost tweak cache is refused by all three entry points that + * take one. The check is in sign as well as agg so the key a member signs + * under is tied to the cache that was validated. */ +static void run_prefractal_identity_cache_test(void) { + prefractal_test_session s; + secp256k1_frost_tweak_cache tweaked; + secp256k1_frost_partial_sig psig; + const secp256k1_frost_partial_sig *psig_ptrs[1]; + secp256k1_musig_partial_sig out; + secp256k1_xonly_pubkey ignored; + unsigned char tweak[32]; + + prefractal_test_setup(&s, 3, 2, prefractal_test_seckey_odd, 1, NULL); + testrand256(tweak); + tweaked = s.tweak_cache; + CHECK(secp256k1_frost_pubkey_xonly_tweak_add(CTX, &ignored, &tweaked, tweak) == 1); + + CHECK(secp256k1_prefractal_sign(CTX, &psig, &s.secnonces[0], s.secshares[0], s.ids[0], + s.ids, s.pubshares, s.t, &s.aggnonce, &s.thresh_pk, + &tweaked, &s.keyagg_cache, &s.cosigner_aggnonce, s.msg) == 0); + + /* A good share, then verification and aggregation handed the tweaked + * cache. The secnonce above was consumed even on the failure path, so this + * uses the other member's. */ + CHECK(secp256k1_prefractal_sign(CTX, &psig, &s.secnonces[1], s.secshares[1], s.ids[1], + s.ids, s.pubshares, s.t, &s.aggnonce, &s.thresh_pk, + &s.tweak_cache, &s.keyagg_cache, &s.cosigner_aggnonce, + s.msg) == 1); + CHECK(secp256k1_prefractal_partial_sig_verify(CTX, &psig, &s.pubnonces[1], &s.pubshares[1], + s.ids[1], s.ids, s.t, &s.aggnonce, &s.thresh_pk, + &tweaked, &s.keyagg_cache, + &s.cosigner_aggnonce, s.msg) == 0); + psig_ptrs[0] = &psig; + CHECK(secp256k1_prefractal_partial_sig_agg(CTX, &out, NULL, psig_ptrs, 1, &tweaked) == 0); + /* The identity cache is accepted at the same call, so the refusal above is + * about the tweak and not about the arguments in general. */ + CHECK(secp256k1_prefractal_partial_sig_agg(CTX, &out, NULL, psig_ptrs, 1, &s.tweak_cache) == 1); +} + +/* thresh_pk and the tweak cache have to describe the same key. Passing a cache + * built for some other group is the mistake this catches. */ +static void run_prefractal_key_cache_mismatch_test(void) { + prefractal_test_session s; + secp256k1_frost_tweak_cache other_cache; + secp256k1_pubkey other_pk, other_pubshares[8]; + unsigned char other_secshares[8][32]; + secp256k1_frost_partial_sig psig; + + prefractal_test_setup(&s, 3, 2, prefractal_test_seckey_odd, 1, NULL); + CHECK(secp256k1_frost_trusted_dealer_keygen(CTX, other_secshares[0], &other_pk, + other_pubshares, 3, 2, + prefractal_test_seckey_even) == 1); + CHECK(secp256k1_frost_tweak_cache_init(CTX, &other_cache, &other_pk) == 1); + + CHECK(secp256k1_prefractal_sign(CTX, &psig, &s.secnonces[0], s.secshares[0], s.ids[0], + s.ids, s.pubshares, s.t, &s.aggnonce, &s.thresh_pk, + &other_cache, &s.keyagg_cache, &s.cosigner_aggnonce, + s.msg) == 0); +} + +/* A musig pubnonce cannot encode the point at infinity, so a group whose + * aggregate nonce lands there has to be told rather than handed something + * unusable. Either column can do it independently: the first is passed through + * unscaled, the second is scaled by b_frost first. + * + * Both are reached the same way. A pubnonce and its negation sum to infinity + * in both columns at once, so the first column is tested with that pair, and + * for the second the members' second-column points are made to cancel while + * the first column does not. */ +static void run_prefractal_infinity_nonce_test(void) { + prefractal_test_session s; + secp256k1_frost_pubnonce negated; + const secp256k1_frost_pubnonce *ptrs[2]; + secp256k1_musig_pubnonce pubnonce_out; + secp256k1_frost_aggnonce aggnonce_out; + unsigned char ser[66], neg_ser[66]; + secp256k1_pubkey pt; + uint32_t ids[2]; + + prefractal_test_setup(&s, 3, 2, prefractal_test_seckey_odd, 1, NULL); + ids[0] = s.ids[0]; + ids[1] = s.ids[1]; + + /* Negate both columns of member 0's nonce: every column then sums to + * infinity. */ + CHECK(secp256k1_frost_pubnonce_serialize(CTX, ser, &s.pubnonces[0]) == 1); + CHECK(secp256k1_ec_pubkey_parse(CTX, &pt, &ser[0], 33) == 1); + CHECK(secp256k1_ec_pubkey_negate(CTX, &pt) == 1); + { + size_t len = 33; + CHECK(secp256k1_ec_pubkey_serialize(CTX, &neg_ser[0], &len, &pt, SECP256K1_EC_COMPRESSED) == 1); + } + CHECK(secp256k1_ec_pubkey_parse(CTX, &pt, &ser[33], 33) == 1); + CHECK(secp256k1_ec_pubkey_negate(CTX, &pt) == 1); + { + size_t len = 33; + CHECK(secp256k1_ec_pubkey_serialize(CTX, &neg_ser[33], &len, &pt, SECP256K1_EC_COMPRESSED) == 1); + } + CHECK(secp256k1_frost_pubnonce_parse(CTX, &negated, neg_ser) == 1); + + ptrs[0] = &s.pubnonces[0]; + ptrs[1] = &negated; + CHECK(secp256k1_prefractal_nonce_agg(CTX, &pubnonce_out, &aggnonce_out, ptrs, ids, 2, + &s.thresh_pk) == 0); + + /* Now cancel only the second column: first column R1 + R1 is not infinity, + * second column R2 + (-R2) is. This is the scaled component, so it reaches + * the guard only after the b_frost multiplication. */ + memcpy(&neg_ser[0], &ser[0], 33); + CHECK(secp256k1_frost_pubnonce_parse(CTX, &negated, neg_ser) == 1); + CHECK(secp256k1_prefractal_nonce_agg(CTX, &pubnonce_out, &aggnonce_out, ptrs, ids, 2, + &s.thresh_pk) == 0); + + /* The untouched set still works, so the refusals above are about the + * infinities and not about this arrangement of arguments. */ + ptrs[1] = &s.pubnonces[1]; + CHECK(secp256k1_prefractal_nonce_agg(CTX, &pubnonce_out, &aggnonce_out, ptrs, ids, 2, + &s.thresh_pk) == 1); +} + +/* A secnonce is single use. sign wipes it, so the second call cannot produce + * the second signature that would expose the share. The wiped secnonce is + * caught by the magic check inside secp256k1_frost_secnonce_load, which is an + * ARG_CHECK and so reaches the illegal-argument callback rather than returning + * 0 - the same way stock secp256k1_frost_sign behaves. */ +static void run_prefractal_nonce_reuse_test(void) { + prefractal_test_session s; + secp256k1_frost_partial_sig psig; + + prefractal_test_setup(&s, 3, 2, prefractal_test_seckey_odd, 1, NULL); + CHECK(secp256k1_prefractal_sign(CTX, &psig, &s.secnonces[0], s.secshares[0], s.ids[0], + s.ids, s.pubshares, s.t, &s.aggnonce, &s.thresh_pk, + &s.tweak_cache, &s.keyagg_cache, &s.cosigner_aggnonce, + s.msg) == 1); + CHECK(secp256k1_is_zero_array(s.secnonces[0].data, sizeof(s.secnonces[0].data))); + CHECK_ILLEGAL(CTX, secp256k1_prefractal_sign(CTX, &psig, &s.secnonces[0], s.secshares[0], + s.ids[0], s.ids, s.pubshares, s.t, &s.aggnonce, + &s.thresh_pk, &s.tweak_cache, &s.keyagg_cache, + &s.cosigner_aggnonce, s.msg)); + + /* Every call that reaches the secnonce load wipes it, including the ones + * that then fail for another reason. A member whose signing attempt was + * refused must not retry with the same nonce. */ + CHECK(secp256k1_is_zero_array(s.secnonces[1].data, sizeof(s.secnonces[1].data)) == 0); + CHECK(secp256k1_prefractal_sign(CTX, &psig, &s.secnonces[1], s.secshares[1], s.ids[2], + s.ids, s.pubshares, s.t, &s.aggnonce, &s.thresh_pk, + &s.tweak_cache, &s.keyagg_cache, &s.cosigner_aggnonce, + s.msg) == 0); + CHECK(secp256k1_is_zero_array(s.secnonces[1].data, sizeof(s.secnonces[1].data))); +} + +/* Negative controls: shares that are individually well formed and only fail at + * the very end, against the outer aggregate. These are the failures the module + * cannot catch for the caller, so the tests pin where they do surface. */ +static void run_prefractal_negative_control_test(void) { + prefractal_test_session s; + unsigned char sig[64], tweak[32]; + + /* Wrong key order: the group signs under a cache that aggregates the two + * keys in the other order, so a_musig is wrong. Every partial signature is + * valid on its own terms and the final signature does not verify. */ + testrand256(tweak); + prefractal_test_setup(&s, 3, 2, prefractal_test_seckey_odd, 1, NULL); + { + const secp256k1_pubkey *swapped[2]; + secp256k1_musig_keyagg_cache wrong_cache; + secp256k1_xonly_pubkey ignored; + swapped[0] = &s.cosigner_pk; + swapped[1] = &s.thresh_pk; + CHECK(secp256k1_musig_pubkey_agg(CTX, &ignored, &wrong_cache, swapped, 2) == 1); + s.keyagg_cache = wrong_cache; + /* finish() would trip its own internal partial_sig_verify CHECKs only + * if the shares disagreed with the session; they do not, because the + * session is rebuilt from this same wrong cache. What fails is the + * final BIP 340 verification against the ORIGINAL aggregate key. */ + CHECK(prefractal_test_finish(&s, 1, sig) == 0); + } + + /* Missing tweak: the group signs under the untweaked outer cache while the + * signature is checked against the tweaked aggregate key. */ + prefractal_test_setup(&s, 3, 2, prefractal_test_seckey_odd, 1, tweak); + { + const secp256k1_pubkey *pubkeys[2]; + secp256k1_musig_keyagg_cache untweaked; + secp256k1_xonly_pubkey ignored; + pubkeys[0] = &s.thresh_pk; + pubkeys[1] = &s.cosigner_pk; + CHECK(secp256k1_musig_pubkey_agg(CTX, &ignored, &untweaked, pubkeys, 2) == 1); + s.keyagg_cache = untweaked; + CHECK(prefractal_test_finish(&s, 1, sig) == 0); + } +} + +/* Argument checking, and the bounds the module states in its header. */ +static void run_prefractal_api_test(void) { + prefractal_test_session s; + secp256k1_musig_pubnonce pubnonce_out; + secp256k1_frost_aggnonce aggnonce_out; + secp256k1_frost_partial_sig psig; + const secp256k1_frost_partial_sig *psig_ptrs[1]; + secp256k1_musig_partial_sig out; + uint32_t dup_ids[2]; + + prefractal_test_setup(&s, 3, 2, prefractal_test_seckey_even, 1, NULL); + + /* Duplicate identifiers make lambda_i undefined, so nonce_agg refuses + * rather than producing a nonce nobody can sign against. */ + dup_ids[0] = s.ids[0]; + dup_ids[1] = s.ids[0]; + CHECK(secp256k1_prefractal_nonce_agg(CTX, &pubnonce_out, &aggnonce_out, s.pubnonce_ptrs, + dup_ids, 2, &s.thresh_pk) == 0); + /* n_signers out of range is a caller bug, not a peer-influenced value, so + * it takes the illegal-argument path rather than returning 0 - the same + * choice the frost module makes for its own counts. */ + CHECK_ILLEGAL(CTX, secp256k1_prefractal_nonce_agg(CTX, &pubnonce_out, &aggnonce_out, + s.pubnonce_ptrs, s.ids, 0, &s.thresh_pk)); + + /* An id that is not in the signer set. */ + CHECK(secp256k1_prefractal_sign(CTX, &psig, &s.secnonces[0], s.secshares[0], s.ids[2], + s.ids, s.pubshares, s.t, &s.aggnonce, &s.thresh_pk, + &s.tweak_cache, &s.keyagg_cache, &s.cosigner_aggnonce, + s.msg) == 0); + + /* A secret share that does not match the pubshare it is claimed for. */ + CHECK(secp256k1_prefractal_sign(CTX, &psig, &s.secnonces[1], s.secshares[0], s.ids[1], + s.ids, s.pubshares, s.t, &s.aggnonce, &s.thresh_pk, + &s.tweak_cache, &s.keyagg_cache, &s.cosigner_aggnonce, + s.msg) == 0); + + /* Passing NULL pubshares skips that check, which is what the header says it + * does; the signature is still correct. */ + prefractal_test_setup(&s, 3, 2, prefractal_test_seckey_even, 1, NULL); + CHECK(secp256k1_prefractal_sign(CTX, &psig, &s.secnonces[0], s.secshares[0], s.ids[0], + s.ids, NULL, s.t, &s.aggnonce, &s.thresh_pk, + &s.tweak_cache, &s.keyagg_cache, &s.cosigner_aggnonce, + s.msg) == 1); + CHECK(secp256k1_prefractal_partial_sig_verify(CTX, &psig, &s.pubnonces[0], &s.pubshares[0], + s.ids[0], s.ids, s.t, &s.aggnonce, &s.thresh_pk, + &s.tweak_cache, &s.keyagg_cache, + &s.cosigner_aggnonce, s.msg) == 1); + + /* n_sigs out of range, likewise. */ + psig_ptrs[0] = &psig; + CHECK_ILLEGAL(CTX, secp256k1_prefractal_partial_sig_agg(CTX, &out, NULL, psig_ptrs, 0, + &s.tweak_cache)); +} + +/* The tagged hash this module defines. Pinning the midstate here is what keeps + * a rewrite of the constant honest: nothing else in the tree would notice a + * changed b_frost, it would just produce signatures that do not verify. */ +static void run_prefractal_midstate_test(void) { + secp256k1_sha256 sha, sha_tagged; + unsigned char tag[] = "Prefractal/noncecoef"; + unsigned char buf[32], buf_tagged[32]; + + secp256k1_sha256_initialize_tagged(secp256k1_get_hash_context(CTX), &sha, tag, sizeof(tag) - 1); + secp256k1_prefractal_noncecoef_sha256_tagged(&sha_tagged); + secp256k1_sha256_finalize(secp256k1_get_hash_context(CTX), &sha, buf); + secp256k1_sha256_finalize(secp256k1_get_hash_context(CTX), &sha_tagged, buf_tagged); + CHECK(secp256k1_memcmp_var(buf, buf_tagged, 32) == 0); } static const struct tf_test_entry tests_prefractal[] = { - CASE1(run_prefractal_smoke_test), + CASE1(run_prefractal_midstate_test), + CASE1(run_prefractal_e2e_test), + CASE1(run_prefractal_odd_y_group_key_test), + CASE1(run_prefractal_partial_sig_verify_test), + CASE1(run_prefractal_identity_cache_test), + CASE1(run_prefractal_key_cache_mismatch_test), + CASE1(run_prefractal_infinity_nonce_test), + CASE1(run_prefractal_nonce_reuse_test), + CASE1(run_prefractal_negative_control_test), + CASE1(run_prefractal_api_test), }; #endif /* SECP256K1_MODULE_PREFRACTAL_TESTS_IMPL_H */