frost_enrollment: close the review's test coverage gaps

Six new tests and repairs to two that were confounded. The suite goes
from 12 cases to 16.

Two existing tests would have stayed green with the checks they target
deleted, which is the worst kind of passing test:

- The "helper id out of range" case also passed new_id = 5 > n = 4,
  which params_are_valid rejects several lines earlier. It now uses a
  valid enrollment target (new_id = 4) and an id of 5, so the id range
  check is the sole failing condition.
- Every call in the empty-set test used (n = 1, t = 2), which fails on
  threshold > n_participants regardless of n_ids. It now uses
  (n = 2, t = 2, new_id = 2) -- a valid tuple in every respect except
  n_ids = 0.

New coverage, in the order the review ranked it:

- run_frost_enrollment_api_test: NULL for every ARG_NONNULL pointer on
  all five entry points, plus an unusable pubkey object on the four that
  take one (previously only params_hash was covered). Also asserts that
  a shares_gen call rejected at an ARG_CHECK does NOT consume the seed,
  since it never reaches the body -- the complement of the "a failed
  call always consumes it" property the previous commit pinned.
- run_frost_enrollment_infinity_test: pubshare_derive's infinity
  rejection, which nothing reached before. With u = 2 and new_id = 2 the
  Lagrange coefficients are exactly -1 and 2, so P_0 = 2*P_1 makes the
  interpolation vanish; the same two points at a different target
  succeed, which is what distinguishes the infinity check from a
  parameter rejection.
- run_frost_enrollment_max_size_test: full protocol runs at the largest
  sizes the API admits -- enrollment at n = 127 with u = 127, and repair
  in a full n = 128 group with u = 127. u cannot reach 128 in either
  mode (enrollment needs n < 128, repair excludes the target from the
  helper set), so these reach one entry below the fixed-size arrays'
  bound, which is as far as a valid tuple goes. Everything before this
  capped at n <= 7. Runs in 51 ms.
- run_frost_enrollment_no_side_effects_test: the C analogue of the
  reference implementation's test_participant_not_in_dkg, which plan
  §1.2 listed and the Phase 3 list dropped. Every existing
  participant's secret share, public share and the group key are
  byte-identical before and after an enrollment, and the new share
  differs from all of them.
- The pubshare_derive test now also pins 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 convention there is checked by running frost's own
  derive_thresh_pubkey over the same loaded points and requiring it to
  reproduce the group key. With the existing check at x = new_id, both
  ends of the interpolation this module depends on are now fixed.

Test-structure repairs the review called out:

- The mismatch test's disagreement now enters where it would in reality,
  at helper 0's round 1.1 call (which is made with new_id = 3 while
  helper 1 uses 4), rather than by running a clean round and
  overwriting the outputs afterwards.
- The oversized-helper-set test re-points a single dealt run at {0,1}
  and then {0,1,2} through a named helper, instead of struct-copying a
  ~540 KB run and hand-editing u and ids[2] -- which would have broken
  silently if deal()'s helper-selection rule changed.
- frost_enrollment_test_run instances in the mismatch test are now
  static. That test needs four live at once, which was over 2 MB of
  stack.
- The randomized test's `if (sub_ids[t-2] >= new_id) continue;` was
  unreachable: the loop bound gives sub_ids[t-2] <= n-1 and new_id == n
  in that branch. It is now the CHECK that states the invariant.
- The pubshare_derive test's `k` was reset and reused as both helper
  counter and aligned-array index inside the same loop body.

Verification: 16/16 pass at -i=16, -i=200 and -i=1000; ./tests,
./noverify_tests and ./exhaustive_tests exit 0; the module is clean
under valgrind (0 errors from 0 contexts).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kgothatso Ngako
2026-09-04 10:21:13 +02:00
parent 266c6a7c4f
commit 69766dd331

View File

@@ -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),
};