diff --git a/src/modules/frost_enrollment/tests_impl.h b/src/modules/frost_enrollment/tests_impl.h index ce5cb480..56183a39 100644 --- a/src/modules/frost_enrollment/tests_impl.h +++ b/src/modules/frost_enrollment/tests_impl.h @@ -97,6 +97,15 @@ static void frost_enrollment_test_round1_agg(frost_enrollment_test_run *r) { } } +/* Re-points an already-dealt run at a different helper set, so that two runs + * can be compared over identical key material without copying the (large) + * run struct or hand-editing its fields. */ +static void frost_enrollment_test_use_helpers(frost_enrollment_test_run *r, const uint32_t *ids, size_t u) { + CHECK(u > 0 && u <= SECP256K1_FROST_MAX_PARTICIPANTS); + memcpy(r->ids, ids, u * sizeof(*ids)); + r->u = u; +} + /* Gathers the helpers' public shares into an array aligned with ids. The * run's own table is indexed by participant id, which only coincides with the * ids alignment when the helper set happens to be 0..u-1 -- exactly the @@ -265,23 +274,29 @@ static void run_frost_enrollment_repair_test(void) { /* An oversized helper set produces the same share: Lagrange interpolation at * the target is exact for any u >= t points on a degree-(t-1) polynomial. */ static void run_frost_enrollment_oversized_set_test(void) { - frost_enrollment_test_run r2, r3; + frost_enrollment_test_run r; unsigned char share_u2[32]; + secp256k1_pubkey pubshare_u2; + static const uint32_t helpers_u2[2] = { 0, 1 }; + static const uint32_t helpers_u3[3] = { 0, 1, 2 }; /* Enroll id 3 into a 2-of-3 group with two helpers, then with all three, - * from the same dealt key material. */ - frost_enrollment_test_full_run(&r2, 3, 2, 2, 3); - memcpy(share_u2, r2.new_secshare, 32); + * over the same dealt key material. */ + frost_enrollment_test_deal(&r, 3, 2, 2, 3); + frost_enrollment_test_use_helpers(&r, helpers_u2, 2); + frost_enrollment_test_round1_gen(&r); + frost_enrollment_test_round1_agg(&r); + frost_enrollment_test_round2(&r); + memcpy(share_u2, r.new_secshare, 32); + pubshare_u2 = r.new_pubshare; - r3 = r2; - r3.u = 3; - r3.ids[2] = 2; - frost_enrollment_test_round1_gen(&r3); - frost_enrollment_test_round1_agg(&r3); - frost_enrollment_test_round2(&r3); - CHECK(secp256k1_memcmp_var(share_u2, r3.new_secshare, 32) == 0); + frost_enrollment_test_use_helpers(&r, helpers_u3, 3); + frost_enrollment_test_round1_gen(&r); + frost_enrollment_test_round1_agg(&r); + frost_enrollment_test_round2(&r); + CHECK(secp256k1_memcmp_var(share_u2, r.new_secshare, 32) == 0); /* The derived public share does not depend on the helper set either. */ - CHECK(secp256k1_memcmp_var(&r2.new_pubshare, &r3.new_pubshare, sizeof(r2.new_pubshare)) == 0); + CHECK(secp256k1_memcmp_var(&pubshare_u2, &r.new_pubshare, sizeof(pubshare_u2)) == 0); } /* A corrupted sigma value must be caught by the public-share check, and the @@ -332,13 +347,13 @@ static void run_frost_enrollment_fault_injection_test(void) { /* Parameter and group agreement, from four angles. */ static void run_frost_enrollment_mismatch_test(void) { - frost_enrollment_test_run r, other; + /* Static rather than automatic: this struct is ~540 KB and this test + * needs several of them live at once. */ + static frost_enrollment_test_run r, other; unsigned char all_shares[2 * 32]; unsigned char received[2 * 32]; unsigned char sigma[32]; unsigned char out[32]; - unsigned char bad_shares[2 * 32]; - unsigned char bad_hash[32]; unsigned char good_hash[32]; unsigned char secrand[32]; uint32_t mismatch_id; @@ -346,16 +361,17 @@ static void run_frost_enrollment_mismatch_test(void) { frost_enrollment_test_deal(&r, 4, 2, 2, 4); - /* (a) Helper 0 runs round 1.1 for a different target. Helper 1's round - * 1.2 must abort and name helper 0 by IDENTIFIER. */ + /* (a) Helper 0 runs round 1.1 believing the target is 3 while helper 1 + * believes it is 4 -- the disagreement enters where it would in reality, + * at the round 1.1 call, rather than being patched in afterwards. + * Helper 1's round 1.2 must abort and name helper 0 by IDENTIFIER. */ CHECK(secp256k1_frost_enrollment_params_hash(CTX, good_hash, &r.thresh_pk, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t) == 1); testrand256(secrand); - CHECK(secp256k1_frost_enrollment_shares_gen(CTX, bad_shares, bad_hash, secrand, r.secshares[0], &r.thresh_pk, r.ids, r.u, r.ids[0], 3, r.n, (uint32_t)r.t) == 1); - CHECK(secp256k1_memcmp_var(bad_hash, good_hash, 32) != 0); - frost_enrollment_test_round1_gen(&r); - CHECK(secp256k1_memcmp_var(r.params_hashes[0], good_hash, 32) == 0); - memcpy(r.params_hashes[0], bad_hash, 32); - memcpy(r.shares[0], bad_shares, sizeof(bad_shares)); + CHECK(secp256k1_frost_enrollment_shares_gen(CTX, r.shares[0], r.params_hashes[0], secrand, r.secshares[r.ids[0]], &r.thresh_pk, r.ids, r.u, r.ids[0], 3, r.n, (uint32_t)r.t) == 1); + testrand256(secrand); + CHECK(secp256k1_frost_enrollment_shares_gen(CTX, r.shares[1], r.params_hashes[1], secrand, r.secshares[r.ids[1]], &r.thresh_pk, r.ids, r.u, r.ids[1], r.new_id, r.n, (uint32_t)r.t) == 1); + CHECK(secp256k1_memcmp_var(r.params_hashes[0], good_hash, 32) != 0); + CHECK(secp256k1_memcmp_var(r.params_hashes[1], good_hash, 32) == 0); frost_enrollment_test_collect(&r, 1, all_shares, received); mismatch_id = 0; @@ -401,7 +417,7 @@ static void run_frost_enrollment_mismatch_test(void) { * different parameters hashes, because the hash commits to the threshold * public key -- and a hash from one group fails round 1.2 in the other. */ { - frost_enrollment_test_run a, b; + static frost_enrollment_test_run a, b; unsigned char hash_a[32], hash_b[32]; frost_enrollment_test_deal(&a, 3, 2, 2, 3); @@ -484,10 +500,13 @@ static void run_frost_enrollment_invalid_params_test(void) { /* threshold above the participant count. */ CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, r.ids, 3, 4, 4, 5) == 0); - /* A helper id outside 0..n-1. */ + /* A helper id outside 0..n-1, and nothing else wrong: new_id = 4 is a + * valid enrollment target for n = 4, and 5 is neither a valid id nor + * equal to new_id, so the id range check is the only condition that + * fails. */ memcpy(ids, r.ids, 3 * sizeof(ids[0])); - ids[2] = 4; - CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, ids, 3, 5, 4, 2) == 0); + ids[2] = 5; + CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, ids, 3, 4, 4, 2) == 0); /* n above the maximum, and n_ids above the maximum. Both must be caught * in production builds; neither may ride on the VERIFY_CHECK inside @@ -540,35 +559,41 @@ static void run_frost_enrollment_invalid_params_test(void) { * zeroed. This also checks that all five symbols are reachable from the test * binary. */ static void run_frost_enrollment_rejects_empty_set_test(void) { - secp256k1_pubkey pk; + frost_enrollment_test_run r; secp256k1_pubkey pubshare; unsigned char buf32[32]; + unsigned char secshare32[32]; unsigned char secrand32[32]; unsigned char hash32[32]; - uint32_t ids[1] = { 0 }; - memset(&pk, 0, sizeof(pk)); + /* (n = 2, t = 2, new_id = 2) is a valid enrollment tuple, so n_ids = 0 is + * the ONLY failing condition here. The earlier version used (n = 1, + * t = 2), which fails on threshold > n_participants whatever n_ids is, + * and would have stayed green with the n_ids handling removed. */ + frost_enrollment_test_deal(&r, 2, 2, 2, 2); + memcpy(secshare32, r.secshares[0], 32); memset(&pubshare, 0xff, sizeof(pubshare)); - memset(secrand32, 0x11, sizeof(secrand32)); + testrand256(secrand32); memset(buf32, 0xff, sizeof(buf32)); - CHECK(secp256k1_frost_enrollment_params_hash(CTX, buf32, &pk, ids, 0, 0, 1, 2) == 0); + CHECK(secp256k1_frost_enrollment_params_hash(CTX, buf32, &r.thresh_pk, r.ids, 0, 2, 2, 2) == 0); CHECK(secp256k1_is_zero_array(buf32, sizeof(buf32))); memset(buf32, 0xff, sizeof(buf32)); memset(hash32, 0xff, sizeof(hash32)); - CHECK(secp256k1_frost_enrollment_shares_gen(CTX, buf32, hash32, secrand32, buf32, &pk, ids, 0, 0, 1, 1, 2) == 0); + CHECK(secp256k1_frost_enrollment_shares_gen(CTX, buf32, hash32, secrand32, secshare32, &r.thresh_pk, r.ids, 0, 0, 2, 2, 2) == 0); CHECK(secp256k1_is_zero_array(hash32, sizeof(hash32))); + CHECK(secp256k1_is_zero_array(secrand32, sizeof(secrand32))); memset(buf32, 0xff, sizeof(buf32)); - CHECK(secp256k1_frost_enrollment_share_agg(CTX, buf32, NULL, buf32, hash32, &pk, ids, 0, 0, 1, 1, 2) == 0); + CHECK(secp256k1_frost_enrollment_share_agg(CTX, buf32, NULL, r.sigmas, hash32, &r.thresh_pk, r.ids, 0, 0, 2, 2, 2) == 0); CHECK(secp256k1_is_zero_array(buf32, sizeof(buf32))); - CHECK(secp256k1_frost_enrollment_pubshare_derive(CTX, &pubshare, &pk, ids, 0, 0, 1, 2) == 0); + CHECK(secp256k1_frost_enrollment_pubshare_derive(CTX, &pubshare, r.pubshares, r.ids, 0, 2, 2, 2) == 0); CHECK(secp256k1_is_zero_array((unsigned char *)&pubshare, sizeof(pubshare))); memset(buf32, 0xff, sizeof(buf32)); - CHECK(secp256k1_frost_enrollment_secshare_gen(CTX, buf32, buf32, &pk, ids, 0, 0, 1, 2, NULL, NULL) == 0); + CHECK(secp256k1_frost_enrollment_secshare_gen(CTX, buf32, r.sigmas, &r.thresh_pk, r.ids, 0, 2, 2, 2, NULL, NULL) == 0); CHECK(secp256k1_is_zero_array(buf32, sizeof(buf32))); } @@ -583,26 +608,22 @@ static void run_frost_enrollment_pubshare_derive_test(void) { secp256k1_gej resultj; secp256k1_scalar x; uint32_t ids[3]; - size_t i, k; + size_t i, j, k; frost_enrollment_test_deal(&r, 4, 3, 3, 4); /* Repair mode at every existing identifier: the derived public share is - * the one the dealer produced. */ + * the one the dealer produced. The helper set is the other three. */ for (i = 0; i < 4; i++) { k = 0; - { - size_t m; - for (m = 0; m < 4 && k < 3; m++) { - if (m != i) { - ids[k] = (uint32_t)m; - k++; - } + for (j = 0; j < 4; j++) { + if (j != i) { + ids[k] = (uint32_t)j; + aligned[k] = r.pubshares[j]; + k++; } } - for (k = 0; k < 3; k++) { - aligned[k] = r.pubshares[ids[k]]; - } + CHECK(k == 3); CHECK(secp256k1_frost_enrollment_pubshare_derive(CTX, &derived, aligned, ids, 3, (uint32_t)i, 4, 3) == 1); CHECK(secp256k1_pubkey_load(CTX, &expected, &r.pubshares[i]) == 1); CHECK(secp256k1_pubkey_load(CTX, &got, &derived) == 1); @@ -610,21 +631,29 @@ static void run_frost_enrollment_pubshare_derive_test(void) { } /* And the wrapper passes the target identifier through unchanged. */ - ids[0] = 0; - ids[1] = 1; - ids[2] = 2; for (k = 0; k < 3; k++) { - aligned[k] = r.pubshares[ids[k]]; + ids[k] = (uint32_t)k; + aligned[k] = r.pubshares[k]; + CHECK(secp256k1_pubkey_load(CTX, &points[k], &r.pubshares[k]) == 1); } CHECK(secp256k1_frost_enrollment_pubshare_derive(CTX, &derived, aligned, ids, 3, 4, 4, 3) == 1); - for (i = 0; i < 3; i++) { - CHECK(secp256k1_pubkey_load(CTX, &points[i], &r.pubshares[i]) == 1); - } secp256k1_scalar_set_int(&x, 4); CHECK(secp256k1_frost_derive_pubshare_at(&resultj, ids, points, 3, &x) == 1); secp256k1_ge_set_gej_var(&expected, &resultj); CHECK(secp256k1_pubkey_load(CTX, &got, &derived) == 1); CHECK(secp256k1_ge_eq_var(&expected, &got) == 1); + + /* The other end of the polynomial. The public API cannot ask for + * x-coordinate 0 -- that is identifier -1, and new_id is a uint32_t + * bounded by n_participants -- so the identifier convention there is + * pinned by evaluating frost's own derive_thresh_pubkey over the same + * points and requiring it to reproduce the group key. Together with the + * check above, this fixes both ends of the interpolation this module + * relies on. */ + CHECK(secp256k1_frost_derive_thresh_pubkey(&resultj, ids, points, 3) == 1); + secp256k1_ge_set_gej_var(&got, &resultj); + CHECK(secp256k1_pubkey_load(CTX, &expected, &r.thresh_pk) == 1); + CHECK(secp256k1_ge_eq_var(&expected, &got) == 1); } /* One random (t, n, u) round trip. Each helper is given the identifier set in @@ -734,9 +763,9 @@ static void frost_enrollment_random_iteration(void) { } sub_ids[t - 1] = new_id; sub_shares[t - 1] = r.new_secshare; - if (sub_ids[t - 2] >= new_id) { - continue; - } + /* The loop bound gives sub_ids[t-2] = i+t-2 <= n-1 < n = new_id, + * so the subset identifiers are always distinct and ascending. */ + CHECK(sub_ids[t - 2] < new_id); frost_enrollment_test_check_reconstruction(sub_ids, sub_shares, t, &r.thresh_pk); } } @@ -951,6 +980,151 @@ static void run_frost_enrollment_contract_test(void) { } } +/* NULL-argument handling for all five entry points: every pointer the header + * marks ARG_NONNULL must fire the illegal-argument callback, and outputs must + * still be left unusable. This mirrors the frost module's api test. */ +static void run_frost_enrollment_api_test(void) { + frost_enrollment_test_run r; + secp256k1_pubkey pubshare; + secp256k1_pubkey helper_pubshares[2]; + unsigned char hash32[32]; + unsigned char shares[2 * 32]; + unsigned char received[2 * 32]; + unsigned char secrand[32]; + unsigned char out[32]; + + frost_enrollment_test_full_run(&r, 3, 2, 2, 3); + frost_enrollment_test_helper_pubshares(&r, helper_pubshares); + memset(received, 0, sizeof(received)); + memcpy(received, r.params_hashes[1], 32); + + CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_params_hash(CTX, NULL, &r.thresh_pk, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t)); + CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_params_hash(CTX, hash32, NULL, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t)); + CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, NULL, r.u, r.new_id, r.n, (uint32_t)r.t)); + + testrand256(secrand); + CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_shares_gen(CTX, NULL, hash32, secrand, r.secshares[0], &r.thresh_pk, r.ids, r.u, r.ids[0], r.new_id, r.n, (uint32_t)r.t)); + CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_shares_gen(CTX, shares, NULL, secrand, r.secshares[0], &r.thresh_pk, r.ids, r.u, r.ids[0], r.new_id, r.n, (uint32_t)r.t)); + CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_shares_gen(CTX, shares, hash32, NULL, r.secshares[0], &r.thresh_pk, r.ids, r.u, r.ids[0], r.new_id, r.n, (uint32_t)r.t)); + CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_shares_gen(CTX, shares, hash32, secrand, NULL, &r.thresh_pk, r.ids, r.u, r.ids[0], r.new_id, r.n, (uint32_t)r.t)); + CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_shares_gen(CTX, shares, hash32, secrand, r.secshares[0], NULL, r.ids, r.u, r.ids[0], r.new_id, r.n, (uint32_t)r.t)); + CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_shares_gen(CTX, shares, hash32, secrand, r.secshares[0], &r.thresh_pk, NULL, r.u, r.ids[0], r.new_id, r.n, (uint32_t)r.t)); + /* None of those consumed the seed: they never reached the body. */ + CHECK(!secp256k1_is_zero_array(secrand, sizeof(secrand))); + + CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_share_agg(CTX, NULL, NULL, r.shares[0], received, &r.thresh_pk, r.ids, r.u, r.ids[1], r.new_id, r.n, (uint32_t)r.t)); + CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_share_agg(CTX, out, NULL, NULL, received, &r.thresh_pk, r.ids, r.u, r.ids[1], r.new_id, r.n, (uint32_t)r.t)); + CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_share_agg(CTX, out, NULL, r.shares[0], NULL, &r.thresh_pk, r.ids, r.u, r.ids[1], r.new_id, r.n, (uint32_t)r.t)); + CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_share_agg(CTX, out, NULL, r.shares[0], received, NULL, r.ids, r.u, r.ids[1], r.new_id, r.n, (uint32_t)r.t)); + CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_share_agg(CTX, out, NULL, r.shares[0], received, &r.thresh_pk, NULL, r.u, r.ids[1], r.new_id, r.n, (uint32_t)r.t)); + + CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_pubshare_derive(CTX, NULL, helper_pubshares, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t)); + CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_pubshare_derive(CTX, &pubshare, NULL, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t)); + CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_pubshare_derive(CTX, &pubshare, helper_pubshares, NULL, r.u, r.new_id, r.n, (uint32_t)r.t)); + + CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_secshare_gen(CTX, NULL, r.sigmas, &r.thresh_pk, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t, NULL, NULL)); + CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_secshare_gen(CTX, out, NULL, &r.thresh_pk, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t, NULL, NULL)); + CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_secshare_gen(CTX, out, r.sigmas, NULL, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t, NULL, NULL)); + CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_secshare_gen(CTX, out, r.sigmas, &r.thresh_pk, NULL, r.u, r.new_id, r.n, (uint32_t)r.t, NULL, NULL)); + + /* An unusable pubkey object is API misuse everywhere it is accepted, not + * just in params_hash. */ + { + secp256k1_pubkey zero_pk; + memset(&zero_pk, 0, sizeof(zero_pk)); + testrand256(secrand); + CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_shares_gen(CTX, shares, hash32, secrand, r.secshares[0], &zero_pk, r.ids, r.u, r.ids[0], r.new_id, r.n, (uint32_t)r.t)); + CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_share_agg(CTX, out, NULL, r.shares[0], received, &zero_pk, r.ids, r.u, r.ids[1], r.new_id, r.n, (uint32_t)r.t)); + CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_pubshare_derive(CTX, &pubshare, &zero_pk, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t)); + CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_secshare_gen(CTX, out, r.sigmas, &zero_pk, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t, NULL, NULL)); + CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_secshare_gen(CTX, out, r.sigmas, &r.thresh_pk, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t, NULL, &zero_pk)); + } +} + +/* pubshare_derive must reject an input set whose interpolation lands on the + * point at infinity. With u = 2 and new_id = 2 the coefficients are -1 and 2, + * so P_0 = 2*P_1 makes the sum vanish. */ +static void run_frost_enrollment_infinity_test(void) { + secp256k1_pubkey pubshares[2], out; + unsigned char sk1[32], sk0[32]; + uint32_t ids[2] = { 0, 1 }; + + do { + testrand256(sk1); + } while (!secp256k1_ec_seckey_verify(CTX, sk1)); + memcpy(sk0, sk1, 32); + /* sk0 = 2 * sk1 */ + CHECK(secp256k1_ec_seckey_tweak_add(CTX, sk0, sk1) == 1); + CHECK(secp256k1_ec_pubkey_create(CTX, &pubshares[0], sk0) == 1); + CHECK(secp256k1_ec_pubkey_create(CTX, &pubshares[1], sk1) == 1); + + memset(&out, 0xff, sizeof(out)); + CHECK(secp256k1_frost_enrollment_pubshare_derive(CTX, &out, pubshares, ids, 2, 2, 3, 2) == 0); + CHECK(secp256k1_is_zero_array((unsigned char *)&out, sizeof(out))); + + /* The same points at a different target do not vanish, so the rejection + * above is the infinity check and not a parameter problem. */ + CHECK(secp256k1_frost_enrollment_pubshare_derive(CTX, &out, pubshares, ids, 2, 3, 4, 2) == 1); +} + +/* Enrollment leaves every existing participant's key material untouched -- + * the C analogue of the reference implementation's test_participant_not_in_dkg, + * which checks that the new participant holds only its aggregate share and the + * group key. */ +static void run_frost_enrollment_no_side_effects_test(void) { + frost_enrollment_test_run r; + unsigned char secshares_before[3][32]; + secp256k1_pubkey pubshares_before[3]; + secp256k1_pubkey thresh_pk_before; + size_t i; + + frost_enrollment_test_deal(&r, 3, 2, 2, 3); + memcpy(secshares_before, r.secshares, sizeof(secshares_before)); + memcpy(pubshares_before, r.pubshares, sizeof(pubshares_before)); + thresh_pk_before = r.thresh_pk; + + frost_enrollment_test_round1_gen(&r); + frost_enrollment_test_round1_agg(&r); + frost_enrollment_test_round2(&r); + + for (i = 0; i < 3; i++) { + CHECK(secp256k1_memcmp_var(r.secshares[i], secshares_before[i], 32) == 0); + CHECK(secp256k1_memcmp_var(&r.pubshares[i], &pubshares_before[i], sizeof(r.pubshares[i])) == 0); + } + CHECK(secp256k1_memcmp_var(&r.thresh_pk, &thresh_pk_before, sizeof(r.thresh_pk)) == 0); + /* And the new share is genuinely new. */ + for (i = 0; i < 3; i++) { + CHECK(secp256k1_memcmp_var(r.new_secshare, r.secshares[i], 32) != 0); + } +} + +/* Full protocol runs at the largest sizes the API admits. u is capped at 127 + * in both modes -- enrollment needs n < 128 and repair excludes the target + * from the helper set -- so these exercise the fixed-size arrays + * (points[128] in pubshare_derive, sorted_ids[128] in the hash) one entry + * below their bound, which is as far as a valid tuple reaches. */ +static void run_frost_enrollment_max_size_test(void) { + /* ~540 KB per run; static so that this does not sit on the stack. */ + static frost_enrollment_test_run r; + const size_t max = SECP256K1_FROST_MAX_PARTICIPANTS; + + /* Enrollment at the largest group that can still grow. */ + frost_enrollment_test_full_run(&r, max - 1, 2, max - 1, (uint32_t)(max - 1)); + { + static secp256k1_pubkey extended[SECP256K1_FROST_MAX_PARTICIPANTS]; + size_t i; + for (i = 0; i < max - 1; i++) { + extended[i] = r.pubshares[i]; + } + extended[max - 1] = r.new_pubshare; + CHECK(secp256k1_frost_threshold_info_validate(CTX, &r.thresh_pk, extended, max, 2) == 1); + } + + /* Repair in a full group, which enrollment mode refuses. */ + frost_enrollment_test_full_run(&r, max, 2, max - 1, 0); + CHECK(secp256k1_memcmp_var(r.new_secshare, r.secshares[0], 32) == 0); +} + static const struct tf_test_entry tests_frost_enrollment[] = { CASE1(run_frost_enrollment_vectors_test), CASE1(run_frost_enrollment_reconstruction_test), @@ -964,6 +1138,10 @@ static const struct tf_test_entry tests_frost_enrollment[] = { CASE1(run_frost_enrollment_invalid_params_test), CASE1(run_frost_enrollment_rejects_empty_set_test), CASE1(run_frost_enrollment_pubshare_derive_test), + CASE1(run_frost_enrollment_infinity_test), + CASE1(run_frost_enrollment_no_side_effects_test), + CASE1(run_frost_enrollment_max_size_test), + CASE1(run_frost_enrollment_api_test), CASE1(run_frost_enrollment_random_test), };