From 303a7caeaebd6fc41dd73aeea5bbb8ae63bb11ad Mon Sep 17 00:00:00 2001 From: Kgothatso Ngako Date: Fri, 4 Sep 2026 04:24:21 +0200 Subject: [PATCH] frost_enrollment: add the test suite and the regression vectors Fourth of six commits. Twelve tests replacing the Phase 2 smoke test, plus a vector generator and the frozen vectors it produces. The regression vectors are the one part of this worth being precise about, because they are easy to over-claim. FROST enrollment has no BIP and no published vectors, and the reference proof of concept draws its randomness from secrets.randbits, which is not seedable -- so there is nothing to cross-validate against. tools/test_vectors_frost_enrollment_generate.py therefore re-implements the math independently in stdlib-only Python, including the group arithmetic written from the secp256k1 parameters rather than borrowed, and freezes the output. What that buys: the two tag strings, the params hash serialization, the share-splitting derivation and the identifier conventions are now pinned, and changing any of them is a loud vector-breaking change. What it does not buy is evidence of protocol correctness. The generator header comment and the generated file both say so, as does frost_enrollment.md. The vectors passed on the first run against the C code, which is worth recording: two independent implementations agree byte for byte on the params hash, every delta, every sigma, the derived public share and the final share, across four cases (2-of-3 minimal, 2-of-3 oversized at u = 3 > t = 2, a 3-of-5 repair with a deliberately UNSORTED helper set, and a 4-of-6 enrollment), covering both threshold-key Y parities. The algebraic invariants are what actually carry correctness: - Reconstruction (PoC test_generate_frost_share): after a 2-of-3 group enrolls id 3, every pair {i, 3} reconstructs the original threshold secret, and so does the untouched pair {0, 1}. - Signing (PoC test_sign): a real BIP340 signature from {2, 3} verifying against the unchanged threshold public key, with every partial signature individually verified, plus the n -> n+1 bookkeeping -- secp256k1_frost_threshold_info_validate must accept the public share table extended with pubshare_derive's output at n+1. - Repair: byte-for-byte equality with the lost share, and the repaired participant keeps its old public share. - Oversized helper set: u = 3 and u = 2 over the same key material produce the same share and the same derived public share. - Randomized: COUNT iterations over 2 <= t <= u <= n <= 7, half enrollment and half repair, with EVERY HELPER GIVEN THE IDENTIFIER SET IN ITS OWN SHUFFLED ORDER. The params hash must come out identical while the delta buffers stay aligned per helper -- which is the whole point of canonicalizing ids inside the hash and nowhere else. Each iteration then checks every t-subset containing the new participant. The negative tests are organized around what each gate is actually for: - Fault injection flips a bit in one sigma. secshare_gen fails and wipes its output; the same call with expected_pubshare = NULL SUCCEEDS and returns a wrong share. That second assertion is the point -- it is the evidence that the parameter is load-bearing rather than decorative. Tampered public shares are caught earlier, by secp256k1_frost_threshold_info_validate, so the test exercises the recommended flow and not just the module. - Parameter mismatch, four angles: (a) one helper runs round 1.1 for a different target and every other helper's share_agg aborts naming it by identifier; (b) a caller that IGNORES that abort and finishes round 1.2 anyway still cannot produce a usable share, because the public-share check catches the inconsistent sum -- defence in depth, not a test of the test's own control flow; (c) the helpers agree with each other on new_id = 3 while the target expects 4, which round 1.2 cannot see and round 2's own recomputation does; (d) two groups with identical (t, n, ids, new_id) get different hashes, and a hash from one fails share_agg in the other. - Own-slot semantics: filling the caller's own slot of received_params_hashes32 with garbage changes nothing, because it is never read -- but the same garbage in a slot that IS read still aborts. That pair is what makes "recomputation, not string comparison" testable rather than merely asserted. - Invalid parameters, including both deliberate divergences: t = 1 refused, enrollment refused at n = 128 while repair at n = 128 is accepted, n_ids > 128 returning 0 with the output zeroed in a production build. Three bugs found while writing these, all in the tests, all worth naming: - pubshare_derive takes public shares ALIGNED WITH ids, and the test helper was handing it the participant-indexed table. Those coincide exactly when the helper set is 0..u-1, which every test until the repair case used, so the first non-contiguous helper set {0, 2} was what exposed it. There is now one helper that does the gather, with a comment saying which confusion it exists to prevent. - The fault-injection test compared against r.new_secshare without ever running round 2, and the mismatch test compared against r.params_hashes[0] one line before round 1.1 filled it. Both were reads of uninitialized memory that happened to pass; valgrind found both. The randomized test loops COUNT times so -i scales it, following the iceberg module (tests_impl.h:1322) rather than prefractal's run-once convention -- a fuzzing loop that ignores the iteration count is not much of one. Verification: all twelve tests pass at the default iteration count, at -i=200 and at -i=2000; ./tests, ./noverify_tests and ./exhaustive_tests exit 0 with all five FROST-stack modules enabled; the module runs clean under valgrind (0 errors from 0 contexts); ctime_tests is clean under valgrind; regenerating vectors.h reproduces it byte for byte. One note for anyone running these locally: ctime_tests must not be run against a CPPFLAGS='-DVERIFY' build. secp256k1_scalar_verify branches on scalar values, which ctime_tests deliberately marks secret, so every scalar operation in the library reports a finding -- 75997 of them, none in this module. The CI matrix already pairs -DVERIFY with CTIMETESTS: 'no' (.github/workflows/ci.yml:119, :596) for this reason. Co-Authored-By: Claude Opus 5 --- .../frost_enrollment/Makefile.am.include | 1 + src/modules/frost_enrollment/tests_impl.h | 680 +++++++++++++++++- src/modules/frost_enrollment/vectors.h | 101 +++ .../test_vectors_frost_enrollment_generate.py | 359 +++++++++ 4 files changed, 1121 insertions(+), 20 deletions(-) create mode 100644 src/modules/frost_enrollment/vectors.h create mode 100755 tools/test_vectors_frost_enrollment_generate.py diff --git a/src/modules/frost_enrollment/Makefile.am.include b/src/modules/frost_enrollment/Makefile.am.include index f8f13831..3232405f 100644 --- a/src/modules/frost_enrollment/Makefile.am.include +++ b/src/modules/frost_enrollment/Makefile.am.include @@ -2,3 +2,4 @@ include_HEADERS += include/secp256k1_frost_enrollment.h noinst_HEADERS += src/modules/frost_enrollment/main_impl.h noinst_HEADERS += src/modules/frost_enrollment/enrollment_impl.h noinst_HEADERS += src/modules/frost_enrollment/tests_impl.h +noinst_HEADERS += src/modules/frost_enrollment/vectors.h diff --git a/src/modules/frost_enrollment/tests_impl.h b/src/modules/frost_enrollment/tests_impl.h index 159f44d1..a92d6d83 100644 --- a/src/modules/frost_enrollment/tests_impl.h +++ b/src/modules/frost_enrollment/tests_impl.h @@ -7,6 +7,9 @@ #define SECP256K1_MODULE_FROST_ENROLLMENT_TESTS_IMPL_H #include "../../../include/secp256k1_frost_enrollment.h" +#include "../../../include/secp256k1_schnorrsig.h" + +#include "vectors.h" /* Everything one enrollment run needs, so a test can set one up in a line and * then poke at individual pieces. */ @@ -19,7 +22,8 @@ typedef struct { secp256k1_pubkey thresh_pk; uint32_t ids[SECP256K1_FROST_MAX_PARTICIPANTS]; - /* shares[i][j] is what helper ids[i] produced for helper ids[j]. */ + /* shares[i] is helper ids[i]'s round 1.1 output buffer, aligned with ids: + * entry j is what helper ids[i] produced for helper ids[j]. */ unsigned char shares[SECP256K1_FROST_MAX_PARTICIPANTS][SECP256K1_FROST_MAX_PARTICIPANTS * 32]; unsigned char params_hashes[SECP256K1_FROST_MAX_PARTICIPANTS][32]; unsigned char sigmas[SECP256K1_FROST_MAX_PARTICIPANTS * 32]; @@ -58,35 +62,59 @@ static void frost_enrollment_test_round1_gen(frost_enrollment_test_run *r) { unsigned char secrand[32]; testrand256(secrand); CHECK(secp256k1_frost_enrollment_shares_gen(CTX, r->shares[i], r->params_hashes[i], secrand, r->secshares[r->ids[i]], &r->thresh_pk, r->ids, r->u, r->ids[i], r->new_id, r->n, (uint32_t)r->t) == 1); + /* The seed is consumed by the call. */ CHECK(secp256k1_is_zero_array(secrand, sizeof(secrand))); } } -/* Runs round 1.2 for every helper, transposing the round 1.1 outputs on the - * way: helper j aggregates entry j of every helper's share buffer. */ +/* Assembles helper j's round 1.2 inputs out of the round 1.1 outputs: the + * share kept at its own position, the shares received at the others', and the + * received parameters hashes with its own slot left zero. */ +static void frost_enrollment_test_collect(const frost_enrollment_test_run *r, size_t j, unsigned char *all_shares, unsigned char *received) { + size_t i; + + memset(received, 0, r->u * 32); + for (i = 0; i < r->u; i++) { + memcpy(&all_shares[32 * i], &r->shares[i][32 * j], 32); + if (i != j) { + memcpy(&received[32 * i], r->params_hashes[i], 32); + } + } +} + +/* Runs round 1.2 for every helper. */ static void frost_enrollment_test_round1_agg(frost_enrollment_test_run *r) { - size_t i, j; + size_t j; for (j = 0; j < r->u; j++) { unsigned char all_shares[SECP256K1_FROST_MAX_PARTICIPANTS * 32]; unsigned char received[SECP256K1_FROST_MAX_PARTICIPANTS * 32]; uint32_t mismatch_id = 0; - memset(received, 0, r->u * 32); - for (i = 0; i < r->u; i++) { - memcpy(&all_shares[32 * i], &r->shares[i][32 * j], 32); - if (i != j) { - memcpy(&received[32 * i], r->params_hashes[i], 32); - } - } + frost_enrollment_test_collect(r, j, all_shares, received); CHECK(secp256k1_frost_enrollment_share_agg(CTX, &r->sigmas[32 * j], &mismatch_id, all_shares, received, &r->thresh_pk, r->ids, r->u, r->ids[j], r->new_id, r->n, (uint32_t)r->t) == 1); CHECK(mismatch_id == UINT32_MAX); } } +/* 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 + * confusion the API documentation warns about. */ +static void frost_enrollment_test_helper_pubshares(const frost_enrollment_test_run *r, secp256k1_pubkey *out) { + size_t i; + + for (i = 0; i < r->u; i++) { + out[i] = r->pubshares[r->ids[i]]; + } +} + /* Runs round 2, with both optional checks enabled. */ static void frost_enrollment_test_round2(frost_enrollment_test_run *r) { - CHECK(secp256k1_frost_enrollment_pubshare_derive(CTX, &r->new_pubshare, r->pubshares, r->ids, r->u, r->new_id, r->n, (uint32_t)r->t) == 1); + secp256k1_pubkey helper_pubshares[SECP256K1_FROST_MAX_PARTICIPANTS]; + + frost_enrollment_test_helper_pubshares(r, helper_pubshares); + CHECK(secp256k1_frost_enrollment_pubshare_derive(CTX, &r->new_pubshare, helper_pubshares, r->ids, r->u, r->new_id, r->n, (uint32_t)r->t) == 1); CHECK(secp256k1_frost_enrollment_secshare_gen(CTX, r->new_secshare, r->sigmas, &r->thresh_pk, r->ids, r->u, r->new_id, r->n, (uint32_t)r->t, r->params_hashes[0], &r->new_pubshare) == 1); } @@ -120,10 +148,50 @@ static void frost_enrollment_test_check_reconstruction(const uint32_t *ids, cons CHECK(secp256k1_ge_eq_var(&pk, &expected) == 1); } -/* A 2-of-3 group grows to 2-of-4, and the new participant's share sits on the - * same polynomial as the old ones: every threshold-sized subset that contains - * it reconstructs the original threshold secret. */ -static void run_frost_enrollment_smoke_test(void) { +/* Produces and verifies a BIP340 signature with the given signer set. shares + * and pubshares must be aligned with ids. */ +static void frost_enrollment_test_sign(const uint32_t *ids, const unsigned char *const *shares, const secp256k1_pubkey *pubshares, size_t n_signers, size_t n_participants, size_t threshold, const secp256k1_pubkey *thresh_pk) { + secp256k1_frost_tweak_cache cache; + 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_partial_sig partial_sigs[SECP256K1_FROST_MAX_PARTICIPANTS]; + const secp256k1_frost_partial_sig *partial_sig_ptrs[SECP256K1_FROST_MAX_PARTICIPANTS]; + secp256k1_frost_aggnonce aggnonce; + secp256k1_frost_session session; + secp256k1_xonly_pubkey tweaked_pk; + unsigned char tweaked_pk32[32]; + unsigned char msg[32]; + unsigned char sig64[64]; + size_t i; + + testrand256(msg); + CHECK(secp256k1_frost_tweak_cache_init(CTX, &cache, thresh_pk) == 1); + CHECK(secp256k1_frost_tweaked_pubkey_get(CTX, &tweaked_pk, &cache) == 1); + CHECK(secp256k1_xonly_pubkey_serialize(CTX, tweaked_pk32, &tweaked_pk) == 1); + + for (i = 0; i < n_signers; i++) { + unsigned char secrand[32]; + testrand256(secrand); + CHECK(secp256k1_frost_nonce_gen(CTX, &secnonces[i], &pubnonces[i], secrand, shares[i], &pubshares[i], tweaked_pk32, msg, sizeof(msg), NULL, 0) == 1); + pubnonce_ptrs[i] = &pubnonces[i]; + } + CHECK(secp256k1_frost_nonce_agg(CTX, &aggnonce, NULL, pubnonce_ptrs, n_signers) == 1); + CHECK(secp256k1_frost_session_init(CTX, &session, &aggnonce, ids, pubshares, n_signers, n_participants, (uint32_t)threshold, &cache, msg, sizeof(msg)) == 1); + for (i = 0; i < n_signers; i++) { + CHECK(secp256k1_frost_sign(CTX, &partial_sigs[i], &secnonces[i], shares[i], &session, ids, pubshares, n_signers, ids[i]) == 1); + CHECK(secp256k1_frost_partial_sig_verify(CTX, &partial_sigs[i], &pubnonces[i], &pubshares[i], &session, ids, n_signers, i) == 1); + partial_sig_ptrs[i] = &partial_sigs[i]; + } + CHECK(secp256k1_frost_partial_sig_agg(CTX, sig64, NULL, &session, partial_sig_ptrs, n_signers) == 1); + CHECK(secp256k1_schnorrsig_verify(CTX, sig64, msg, sizeof(msg), &tweaked_pk) == 1); +} + +/* PoC test_generate_frost_share: a 2-of-3 group grows to 2-of-4, and the new + * share sits on the same polynomial as the old ones. Every threshold-sized + * subset containing the new participant reconstructs the original threshold + * secret, and so does the untouched original pair. */ +static void run_frost_enrollment_reconstruction_test(void) { frost_enrollment_test_run r; const unsigned char *shares[2]; uint32_t ids[2]; @@ -131,8 +199,6 @@ static void run_frost_enrollment_smoke_test(void) { frost_enrollment_test_full_run(&r, 3, 2, 2, 3); - /* The pair that ran the protocol, and every other pair including the new - * participant. */ for (i = 0; i < 3; i++) { ids[0] = (uint32_t)i; ids[1] = 3; @@ -140,7 +206,6 @@ static void run_frost_enrollment_smoke_test(void) { shares[1] = r.new_secshare; frost_enrollment_test_check_reconstruction(ids, shares, 2, &r.thresh_pk); } - /* And the old group still reconstructs, unchanged. */ ids[0] = 0; ids[1] = 1; shares[0] = r.secshares[0]; @@ -148,6 +213,329 @@ static void run_frost_enrollment_smoke_test(void) { frost_enrollment_test_check_reconstruction(ids, shares, 2, &r.thresh_pk); } +/* PoC test_sign: a real BIP340 signature from a signer set that includes the + * enrolled participant, over the unchanged threshold public key. Also the + * n -> n+1 bookkeeping: the extended public share table must still satisfy + * secp256k1_frost_threshold_info_validate at n+1. */ +static void run_frost_enrollment_signing_test(void) { + frost_enrollment_test_run r; + secp256k1_pubkey pubshares[4]; + const unsigned char *shares[2]; + uint32_t ids[2]; + size_t i; + + frost_enrollment_test_full_run(&r, 3, 2, 2, 3); + + /* The extended table: the three original public shares plus the derived + * one at the new identifier. */ + for (i = 0; i < 3; i++) { + pubshares[i] = r.pubshares[i]; + } + pubshares[3] = r.new_pubshare; + CHECK(secp256k1_frost_threshold_info_validate(CTX, &r.thresh_pk, pubshares, 4, 2) == 1); + + /* Signer set {2, 3}: one original participant and the new one. */ + ids[0] = 2; + ids[1] = 3; + shares[0] = r.secshares[2]; + shares[1] = r.new_secshare; + { + secp256k1_pubkey signer_pubshares[2]; + signer_pubshares[0] = pubshares[2]; + signer_pubshares[1] = pubshares[3]; + frost_enrollment_test_sign(ids, shares, signer_pubshares, 2, 4, 2, &r.thresh_pk); + } +} + +/* Repair mode: participant 1 "loses" its share and the same protocol run at + * new_id = 1 reproduces it, byte for byte. The share is f(x_1), a fixed value, + * not a fresh random one, so anything short of exact equality is a bug. */ +static void run_frost_enrollment_repair_test(void) { + frost_enrollment_test_run r; + + frost_enrollment_test_full_run(&r, 3, 2, 2, 1); + /* The helper set is {0, 2}: deal() skips the target identifier. */ + CHECK(r.ids[0] == 0); + CHECK(r.ids[1] == 2); + CHECK(secp256k1_memcmp_var(r.new_secshare, r.secshares[1], 32) == 0); + /* And the repaired participant keeps its old public share. */ + CHECK(secp256k1_memcmp_var(&r.new_pubshare, &r.pubshares[1], sizeof(r.new_pubshare)) == 0); +} + +/* 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; + unsigned char share_u2[32]; + + /* 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); + + 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); + /* 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); +} + +/* A corrupted sigma value must be caught by the public-share check, and the + * output must be wiped rather than left holding a wrong share. Tampered + * pubshares are caught earlier, by the validation step the recommended flow + * runs before the protocol starts. */ +static void run_frost_enrollment_fault_injection_test(void) { + frost_enrollment_test_run r; + unsigned char sigmas[2 * 32]; + unsigned char out[32]; + secp256k1_pubkey tampered[3]; + size_t i; + + /* A clean run first, so that r.new_secshare holds the share the corrupted + * runs below must fail to reproduce. */ + frost_enrollment_test_full_run(&r, 3, 2, 2, 3); + + for (i = 0; i < 2; i++) { + memcpy(sigmas, r.sigmas, sizeof(sigmas)); + sigmas[32 * i] ^= 1; + memset(out, 0xff, sizeof(out)); + CHECK(secp256k1_frost_enrollment_secshare_gen(CTX, out, sigmas, &r.thresh_pk, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t, r.params_hashes[0], &r.new_pubshare) == 0); + CHECK(secp256k1_is_zero_array(out, sizeof(out))); + /* Without the public-share check nothing notices: the sum is a + * perfectly well-formed scalar, just the wrong one. This is what + * makes expected_pubshare load-bearing rather than optional. */ + memset(out, 0xff, sizeof(out)); + CHECK(secp256k1_frost_enrollment_secshare_gen(CTX, out, sigmas, &r.thresh_pk, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t, r.params_hashes[0], NULL) == 1); + CHECK(secp256k1_memcmp_var(out, r.new_secshare, 32) != 0); + } + + /* The recommended flow, not just the module: a tampered public share is + * rejected by secp256k1_frost_threshold_info_validate against the + * independently authenticated threshold public key, before enrollment + * begins. */ + for (i = 0; i < 3; i++) { + unsigned char ser[33]; + size_t len = sizeof(ser); + memcpy(tampered, r.pubshares, sizeof(tampered)); + CHECK(secp256k1_ec_pubkey_serialize(CTX, ser, &len, &tampered[i], SECP256K1_EC_COMPRESSED) == 1); + /* Flip to the other point of the same x-coordinate: still a valid + * pubkey, but no longer on the group's polynomial. */ + ser[0] ^= 1; + CHECK(secp256k1_ec_pubkey_parse(CTX, &tampered[i], ser, len) == 1); + CHECK(secp256k1_frost_threshold_info_validate(CTX, &r.thresh_pk, tampered, 3, 2) == 0); + } +} + +/* Parameter and group agreement, from four angles. */ +static void run_frost_enrollment_mismatch_test(void) { + 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; + size_t j; + + 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. */ + 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)); + + frost_enrollment_test_collect(&r, 1, all_shares, received); + mismatch_id = 0; + memset(sigma, 0xff, sizeof(sigma)); + CHECK(secp256k1_frost_enrollment_share_agg(CTX, sigma, &mismatch_id, all_shares, received, &r.thresh_pk, r.ids, r.u, r.ids[1], r.new_id, r.n, (uint32_t)r.t) == 0); + CHECK(mismatch_id == r.ids[0]); + CHECK(secp256k1_is_zero_array(sigma, sizeof(sigma))); + + /* (b) A caller that ignores the abort and finishes round 1.2 anyway still + * does not end up with a usable share: the public-share check catches the + * inconsistent sum. Defence in depth, rather than a test of the test's + * own control flow. */ + for (j = 0; j < r.u; j++) { + size_t k; + frost_enrollment_test_collect(&r, j, all_shares, received); + /* Simulate the gate having passed: every helper is handed the hash it + * expects, while helper 0's mismatched delta values stay in place. */ + for (k = 0; k < r.u; k++) { + if (k != j) { + memcpy(&received[32 * k], good_hash, 32); + } + } + CHECK(secp256k1_frost_enrollment_share_agg(CTX, &r.sigmas[32 * j], NULL, all_shares, received, &r.thresh_pk, r.ids, r.u, r.ids[j], r.new_id, r.n, (uint32_t)r.t) == 1); + } + { + secp256k1_pubkey helper_pubshares[SECP256K1_FROST_MAX_PARTICIPANTS]; + frost_enrollment_test_helper_pubshares(&r, helper_pubshares); + CHECK(secp256k1_frost_enrollment_pubshare_derive(CTX, &r.new_pubshare, helper_pubshares, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t) == 1); + } + memset(out, 0xff, sizeof(out)); + CHECK(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, good_hash, &r.new_pubshare) == 0); + CHECK(secp256k1_is_zero_array(out, sizeof(out))); + + /* (c) The helpers agree with each other but not with the target: a clean + * run for new_id = 3, handed to a target that believes it is 4. Round 1.2 + * passed everywhere; round 2's own recomputation is what catches it. */ + frost_enrollment_test_full_run(&other, 4, 2, 2, 3); + memset(out, 0xff, sizeof(out)); + CHECK(secp256k1_frost_enrollment_secshare_gen(CTX, out, other.sigmas, &other.thresh_pk, other.ids, other.u, 4, other.n, (uint32_t)other.t, other.params_hashes[0], NULL) == 0); + CHECK(secp256k1_is_zero_array(out, sizeof(out))); + + /* (d) Group binding. Two groups with identical (t, n, ids, new_id) get + * 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; + unsigned char hash_a[32], hash_b[32]; + + frost_enrollment_test_deal(&a, 3, 2, 2, 3); + frost_enrollment_test_deal(&b, 3, 2, 2, 3); + CHECK(secp256k1_memcmp_var(&a.thresh_pk, &b.thresh_pk, sizeof(a.thresh_pk)) != 0); + CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash_a, &a.thresh_pk, a.ids, a.u, a.new_id, a.n, (uint32_t)a.t) == 1); + CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash_b, &b.thresh_pk, b.ids, b.u, b.new_id, b.n, (uint32_t)b.t) == 1); + CHECK(secp256k1_memcmp_var(hash_a, hash_b, 32) != 0); + + frost_enrollment_test_round1_gen(&a); + frost_enrollment_test_collect(&a, 1, all_shares, received); + memcpy(&received[0], hash_b, 32); + mismatch_id = 0; + CHECK(secp256k1_frost_enrollment_share_agg(CTX, sigma, &mismatch_id, all_shares, received, &a.thresh_pk, a.ids, a.u, a.ids[1], a.new_id, a.n, (uint32_t)a.t) == 0); + CHECK(mismatch_id == a.ids[0]); + } +} + +/* The own slot of received_params_hashes32 is never read, so a caller cannot + * fill it with a received hash and launder a mismatch into a pass. */ +static void run_frost_enrollment_own_slot_test(void) { + frost_enrollment_test_run r; + unsigned char all_shares[2 * 32]; + unsigned char received[2 * 32]; + unsigned char sigma_zero[32], sigma_garbage[32]; + + frost_enrollment_test_deal(&r, 3, 2, 2, 3); + frost_enrollment_test_round1_gen(&r); + + /* Helper 1 aggregates with its own slot zero, as documented. */ + frost_enrollment_test_collect(&r, 1, all_shares, received); + CHECK(secp256k1_frost_enrollment_share_agg(CTX, sigma_zero, NULL, all_shares, received, &r.thresh_pk, r.ids, r.u, r.ids[1], r.new_id, r.n, (uint32_t)r.t) == 1); + + /* And again with garbage in that slot. Same result: it is not read. */ + memset(&received[32], 0xa5, 32); + CHECK(secp256k1_frost_enrollment_share_agg(CTX, sigma_garbage, NULL, all_shares, received, &r.thresh_pk, r.ids, r.u, r.ids[1], r.new_id, r.n, (uint32_t)r.t) == 1); + CHECK(secp256k1_memcmp_var(sigma_zero, sigma_garbage, 32) == 0); + + /* But a wrong hash in a slot that IS read still aborts, even if the same + * wrong hash sits in the own slot -- the own hash is recomputed, so there + * is nothing to agree with. */ + memset(&received[0], 0xa5, 32); + CHECK(secp256k1_frost_enrollment_share_agg(CTX, sigma_garbage, NULL, all_shares, received, &r.thresh_pk, r.ids, r.u, r.ids[1], r.new_id, r.n, (uint32_t)r.t) == 0); +} + +/* Invalid parameter tuples, including the two deliberate divergences from the + * frost module (threshold >= 2, and enrollment refused at n = 128). */ +static void run_frost_enrollment_invalid_params_test(void) { + frost_enrollment_test_run r; + unsigned char hash32[32]; + unsigned char shares[4 * 32]; + unsigned char secrand[32]; + uint32_t ids[4]; + + frost_enrollment_test_deal(&r, 4, 2, 3, 4); + + /* The valid baseline. */ + CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, r.ids, 3, 4, 4, 2) == 1); + + /* Duplicate ids. */ + memcpy(ids, r.ids, 3 * sizeof(ids[0])); + ids[2] = ids[0]; + memset(hash32, 0xff, sizeof(hash32)); + CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, ids, 3, 4, 4, 2) == 0); + CHECK(secp256k1_is_zero_array(hash32, sizeof(hash32))); + + /* new_id among the helpers. */ + CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, r.ids, 3, r.ids[1], 4, 2) == 0); + + /* new_id past the end: neither enrollment (== n) nor repair (< n). */ + CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, r.ids, 3, 5, 4, 2) == 0); + + /* Too few helpers, and more helpers than participants. */ + CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, r.ids, 1, 4, 4, 2) == 0); + CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, r.ids, 3, 4, 2, 2) == 0); + + /* threshold = 1 is refused, unlike in the frost module. */ + CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, r.ids, 3, 4, 4, 1) == 0); + CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, r.ids, 3, 4, 4, 0) == 0); + /* 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. */ + 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); + + /* 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 + * secp256k1_frost_sort_ids. */ + CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, r.ids, 3, 4, SECP256K1_FROST_MAX_PARTICIPANTS + 1, 2) == 0); + memset(hash32, 0xff, sizeof(hash32)); + CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, r.ids, SECP256K1_FROST_MAX_PARTICIPANTS + 1, 4, 4, 2) == 0); + CHECK(secp256k1_is_zero_array(hash32, sizeof(hash32))); + + /* Mode-specific bounds at the maximum: enrollment would produce a + * 129-participant group and is refused; repair leaves n alone and is + * accepted. Both use a helper set of exactly t, so no oversized array is + * involved either way. */ + { + uint32_t big_ids[2]; + big_ids[0] = 0; + big_ids[1] = 1; + CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, big_ids, 2, SECP256K1_FROST_MAX_PARTICIPANTS, SECP256K1_FROST_MAX_PARTICIPANTS, 2) == 0); + CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, big_ids, 2, SECP256K1_FROST_MAX_PARTICIPANTS - 1, SECP256K1_FROST_MAX_PARTICIPANTS, 2) == 1); + /* One below the maximum, enrollment is fine again. */ + CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, big_ids, 2, SECP256K1_FROST_MAX_PARTICIPANTS - 1, SECP256K1_FROST_MAX_PARTICIPANTS - 1, 2) == 1); + } + + /* An uninitialized threshold public key: secp256k1_pubkey_load treats + * that as an API misuse and fires the illegal-argument callback, as it + * does everywhere else in the library. */ + { + secp256k1_pubkey zero_pk; + memset(&zero_pk, 0, sizeof(zero_pk)); + CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_params_hash(CTX, hash32, &zero_pk, r.ids, 3, 4, 4, 2)); + CHECK(secp256k1_is_zero_array(hash32, sizeof(hash32))); + } + + /* The other entry points reject the same tuples. */ + testrand256(secrand); + CHECK(secp256k1_frost_enrollment_shares_gen(CTX, shares, hash32, secrand, r.secshares[0], &r.thresh_pk, r.ids, 3, r.ids[0], 4, 4, 1) == 0); + CHECK(secp256k1_is_zero_array(secrand, sizeof(secrand))); + /* my_id must be one of the helpers. */ + testrand256(secrand); + CHECK(secp256k1_frost_enrollment_shares_gen(CTX, shares, hash32, secrand, r.secshares[0], &r.thresh_pk, r.ids, 3, 3, 4, 4, 2) == 0); + { + secp256k1_pubkey helper_pubshares[SECP256K1_FROST_MAX_PARTICIPANTS]; + frost_enrollment_test_helper_pubshares(&r, helper_pubshares); + CHECK(secp256k1_frost_enrollment_pubshare_derive(CTX, &r.new_pubshare, helper_pubshares, r.ids, 3, 4, 4, 1) == 0); + } + CHECK(secp256k1_frost_enrollment_secshare_gen(CTX, shares, r.sigmas, &r.thresh_pk, r.ids, 3, 4, 4, 1, NULL, NULL) == 0); +} + /* Every entry point must reject an empty helper set and leave its output * zeroed. This also checks that all five symbols are reachable from the test * binary. */ @@ -184,9 +572,261 @@ static void run_frost_enrollment_rejects_empty_set_test(void) { CHECK(secp256k1_is_zero_array(buf32, sizeof(buf32))); } +/* pubshare_derive is a wrapper over frost's derive_pubshare_at. This pins its + * argument plumbing: at an existing participant's identifier it must return + * exactly that participant's public share, and it must agree with a direct + * call to the function it wraps. */ +static void run_frost_enrollment_pubshare_derive_test(void) { + frost_enrollment_test_run r; + secp256k1_pubkey derived, aligned[3]; + secp256k1_ge points[3], expected, got; + secp256k1_gej resultj; + secp256k1_scalar x; + uint32_t ids[3]; + size_t i, 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. */ + 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 (k = 0; k < 3; k++) { + aligned[k] = r.pubshares[ids[k]]; + } + 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); + CHECK(secp256k1_ge_eq_var(&expected, &got) == 1); + } + + /* 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]]; + } + 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); +} + +/* One random (t, n, u) round trip. Each helper is given the identifier set in + * its own random order, which must not change the parameters hash -- while the + * delta buffers stay aligned with whatever order that helper used. */ +static void frost_enrollment_random_iteration(void) { + frost_enrollment_test_run r; + uint32_t perm[SECP256K1_FROST_MAX_PARTICIPANTS][SECP256K1_FROST_MAX_PARTICIPANTS]; + unsigned char hash32[32]; + size_t n, t, u, i, j; + uint32_t new_id; + + /* 2 <= t <= u <= n <= 7 */ + t = 2 + testrand_int(3); + u = t + testrand_int(4); + n = u + testrand_int(8 - (unsigned int)u); + if (n > 7) { + n = 7; + } + if (u > n) { + u = n; + } + /* Enrollment half the time, repair the other half. */ + new_id = testrand_bits(1) ? (uint32_t)n : (uint32_t)testrand_int((unsigned int)n); + if ((size_t)new_id < n && u > n - 1) { + u = n - 1; + } + if (u < t) { + return; + } + + frost_enrollment_test_deal(&r, n, t, u, new_id); + + /* Give every helper its own shuffled view of the identifier set. */ + for (i = 0; i < u; i++) { + memcpy(perm[i], r.ids, u * sizeof(r.ids[0])); + for (j = u; j > 1; j--) { + size_t k = testrand_int((unsigned int)j); + uint32_t tmp = perm[i][j - 1]; + perm[i][j - 1] = perm[i][k]; + perm[i][k] = tmp; + } + CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, perm[i], u, new_id, n, (uint32_t)t) == 1); + } + + /* Round 1.1 in each helper's own order. */ + for (i = 0; i < u; i++) { + unsigned char secrand[32]; + testrand256(secrand); + CHECK(secp256k1_frost_enrollment_shares_gen(CTX, r.shares[i], r.params_hashes[i], secrand, r.secshares[r.ids[i]], &r.thresh_pk, perm[i], u, r.ids[i], new_id, n, (uint32_t)t) == 1); + /* Order-independent: same digest as the canonical order. */ + CHECK(secp256k1_memcmp_var(r.params_hashes[i], hash32, 32) == 0); + } + + /* Round 1.2, translating each helper's alignment into the canonical one. + * all_shares[k] must be what helper ids[k] produced for helper ids[j], + * which sits at helper k's own position for ids[j]. */ + for (j = 0; j < u; j++) { + unsigned char all_shares[SECP256K1_FROST_MAX_PARTICIPANTS * 32]; + unsigned char received[SECP256K1_FROST_MAX_PARTICIPANTS * 32]; + + memset(received, 0, u * 32); + for (i = 0; i < u; i++) { + size_t pos; + for (pos = 0; pos < u; pos++) { + if (perm[i][pos] == r.ids[j]) { + break; + } + } + CHECK(pos < u); + memcpy(&all_shares[32 * i], &r.shares[i][32 * pos], 32); + if (i != j) { + memcpy(&received[32 * i], r.params_hashes[i], 32); + } + } + CHECK(secp256k1_frost_enrollment_share_agg(CTX, &r.sigmas[32 * j], NULL, all_shares, received, &r.thresh_pk, r.ids, u, r.ids[j], new_id, n, (uint32_t)t) == 1); + } + + { + secp256k1_pubkey helper_pubshares[SECP256K1_FROST_MAX_PARTICIPANTS]; + frost_enrollment_test_helper_pubshares(&r, helper_pubshares); + CHECK(secp256k1_frost_enrollment_pubshare_derive(CTX, &r.new_pubshare, helper_pubshares, r.ids, u, new_id, n, (uint32_t)t) == 1); + } + CHECK(secp256k1_frost_enrollment_secshare_gen(CTX, r.new_secshare, r.sigmas, &r.thresh_pk, r.ids, u, new_id, n, (uint32_t)t, hash32, &r.new_pubshare) == 1); + + if ((size_t)new_id < n) { + /* Repair reproduces the lost share exactly. */ + CHECK(secp256k1_memcmp_var(r.new_secshare, r.secshares[new_id], 32) == 0); + } else { + /* Enrollment: every t-subset of the extended group that contains the + * new participant reconstructs the same threshold secret, and the + * extended public share table still validates at n+1. */ + secp256k1_pubkey extended[SECP256K1_FROST_MAX_PARTICIPANTS]; + uint32_t sub_ids[SECP256K1_FROST_MAX_PARTICIPANTS]; + const unsigned char *sub_shares[SECP256K1_FROST_MAX_PARTICIPANTS]; + + for (i = 0; i < n; i++) { + extended[i] = r.pubshares[i]; + } + extended[n] = r.new_pubshare; + CHECK(secp256k1_frost_threshold_info_validate(CTX, &r.thresh_pk, extended, n + 1, (uint32_t)t) == 1); + + for (i = 0; i + t <= n + 1; i++) { + for (j = 0; j + 1 < t; j++) { + sub_ids[j] = (uint32_t)(i + j); + sub_shares[j] = r.secshares[i + j]; + } + sub_ids[t - 1] = new_id; + sub_shares[t - 1] = r.new_secshare; + if (sub_ids[t - 2] >= new_id) { + continue; + } + frost_enrollment_test_check_reconstruction(sub_ids, sub_shares, t, &r.thresh_pk); + } + } +} + +/* The frozen regression vectors. They pin the two tag strings, the parameters + * hash encoding and the share-splitting derivation: any change to those is a + * vector-breaking change, and this is where it shows up. */ +static void run_frost_enrollment_vectors_test(void) { + size_t c; + + for (c = 0; c < sizeof(frost_enrollment_vec_cases) / sizeof(frost_enrollment_vec_cases[0]); c++) { + const struct frost_enrollment_vec_case *v = &frost_enrollment_vec_cases[c]; + secp256k1_pubkey thresh_pk, pubshares[SECP256K1_FROST_MAX_PARTICIPANTS], new_pubshare; + unsigned char hash32[32]; + unsigned char shares[SECP256K1_FROST_MAX_PARTICIPANTS * 32]; + unsigned char all_shares[SECP256K1_FROST_MAX_PARTICIPANTS * 32]; + unsigned char received[SECP256K1_FROST_MAX_PARTICIPANTS * 32]; + unsigned char sigmas[SECP256K1_FROST_MAX_PARTICIPANTS * 32]; + unsigned char secshare[32]; + unsigned char secrand[32]; + unsigned char ser[33]; + size_t len; + size_t i, j; + + CHECK(secp256k1_ec_pubkey_parse(CTX, &thresh_pk, v->thresh_pk33, 33) == 1); + for (i = 0; i < v->n_ids; i++) { + CHECK(secp256k1_ec_pubkey_parse(CTX, &pubshares[i], v->pubshares33[i], 33) == 1); + } + + /* The parameters hash. */ + CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &thresh_pk, v->ids, v->n_ids, v->new_id, v->n_participants, v->threshold) == 1); + CHECK(secp256k1_memcmp_var(hash32, v->params_hash32, 32) == 0); + + /* Round 1.1 for every helper, from the frozen seeds. */ + for (i = 0; i < v->n_ids; i++) { + memcpy(secrand, v->session_secrand32[i], 32); + CHECK(secp256k1_frost_enrollment_shares_gen(CTX, shares, hash32, secrand, v->secshares32[i], &thresh_pk, v->ids, v->n_ids, v->ids[i], v->new_id, v->n_participants, v->threshold) == 1); + CHECK(secp256k1_memcmp_var(hash32, v->params_hash32, 32) == 0); + CHECK(secp256k1_memcmp_var(shares, v->shares32[i], v->n_ids * 32) == 0); + } + + /* Round 1.2 for every helper. */ + for (j = 0; j < v->n_ids; j++) { + memset(received, 0, v->n_ids * 32); + for (i = 0; i < v->n_ids; i++) { + memcpy(&all_shares[32 * i], &v->shares32[i][32 * j], 32); + if (i != j) { + memcpy(&received[32 * i], v->params_hash32, 32); + } + } + CHECK(secp256k1_frost_enrollment_share_agg(CTX, &sigmas[32 * j], NULL, all_shares, received, &thresh_pk, v->ids, v->n_ids, v->ids[j], v->new_id, v->n_participants, v->threshold) == 1); + } + CHECK(secp256k1_memcmp_var(sigmas, v->sigmas32, v->n_ids * 32) == 0); + + /* The derived public share and round 2. */ + CHECK(secp256k1_frost_enrollment_pubshare_derive(CTX, &new_pubshare, pubshares, v->ids, v->n_ids, v->new_id, v->n_participants, v->threshold) == 1); + len = sizeof(ser); + CHECK(secp256k1_ec_pubkey_serialize(CTX, ser, &len, &new_pubshare, SECP256K1_EC_COMPRESSED) == 1); + CHECK(len == 33); + CHECK(secp256k1_memcmp_var(ser, v->new_pubshare33, 33) == 0); + + CHECK(secp256k1_frost_enrollment_secshare_gen(CTX, secshare, sigmas, &thresh_pk, v->ids, v->n_ids, v->new_id, v->n_participants, v->threshold, v->params_hash32, &new_pubshare) == 1); + CHECK(secp256k1_memcmp_var(secshare, v->new_secshare32, 32) == 0); + } +} + +/* COUNT iterations of the above, so that -i scales the fuzzing the way it does + * for the iceberg module's randomized loops. */ +static void run_frost_enrollment_random_test(void) { + int i; + + for (i = 0; i < COUNT; i++) { + frost_enrollment_random_iteration(); + } +} + static const struct tf_test_entry tests_frost_enrollment[] = { - CASE1(run_frost_enrollment_smoke_test), + CASE1(run_frost_enrollment_vectors_test), + CASE1(run_frost_enrollment_reconstruction_test), + CASE1(run_frost_enrollment_signing_test), + CASE1(run_frost_enrollment_repair_test), + CASE1(run_frost_enrollment_oversized_set_test), + CASE1(run_frost_enrollment_fault_injection_test), + CASE1(run_frost_enrollment_mismatch_test), + CASE1(run_frost_enrollment_own_slot_test), + 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_random_test), }; #endif /* SECP256K1_MODULE_FROST_ENROLLMENT_TESTS_IMPL_H */ diff --git a/src/modules/frost_enrollment/vectors.h b/src/modules/frost_enrollment/vectors.h new file mode 100644 index 00000000..cd48e450 --- /dev/null +++ b/src/modules/frost_enrollment/vectors.h @@ -0,0 +1,101 @@ +/** + * Automatically generated by tools/test_vectors_frost_enrollment_generate.py. + * + * REGRESSION vectors, not cross-validation vectors. FROST enrollment has no + * BIP and no published test vectors, and the reference proof of concept + * (https://github.com/siv2r/frost-enrollment) draws its randomness from + * secrets.randbits, which is not seedable, so there is nothing to check + * against. The generator re-implements the math independently in Python and + * freezes the result. + * + * What these pin: the two tag strings ("FROST enrollment/params_hash" and + * "FROST enrollment/share_split"), the parameters hash serialization, the + * share-splitting derivation, and the identifier conventions. Changing any of + * them is a vector-breaking change. What they do NOT establish is protocol + * correctness -- the algebraic invariants in tests_impl.h carry that. + * + * Used by the tests in src/modules/frost_enrollment/tests_impl.h. */ + +#ifndef SECP256K1_MODULE_FROST_ENROLLMENT_VECTORS_H +#define SECP256K1_MODULE_FROST_ENROLLMENT_VECTORS_H + +#define FROST_ENROLLMENT_VEC_MAX_IDS 4 + +struct frost_enrollment_vec_case { + /* Parameters. */ + size_t n_participants; + uint32_t threshold; + size_t n_ids; + uint32_t new_id; + uint32_t ids[FROST_ENROLLMENT_VEC_MAX_IDS]; + /* Group key material, aligned with ids. */ + unsigned char thresh_pk33[33]; + unsigned char pubshares33[FROST_ENROLLMENT_VEC_MAX_IDS][33]; + unsigned char secshares32[FROST_ENROLLMENT_VEC_MAX_IDS][32]; + /* Round 1.1 inputs and outputs. shares32[i] is helper ids[i]'s output + * buffer, aligned with ids. */ + unsigned char session_secrand32[FROST_ENROLLMENT_VEC_MAX_IDS][32]; + unsigned char params_hash32[32]; + unsigned char shares32[FROST_ENROLLMENT_VEC_MAX_IDS][FROST_ENROLLMENT_VEC_MAX_IDS * 32]; + /* Round 1.2 and round 2 outputs. */ + unsigned char sigmas32[FROST_ENROLLMENT_VEC_MAX_IDS * 32]; + unsigned char new_secshare32[32]; + unsigned char new_pubshare33[33]; +}; + +static const struct frost_enrollment_vec_case frost_enrollment_vec_cases[4] = { + { + 3, 2, 2, 3, + { 0, 1 }, + { 0x02, 0x4D, 0x4B, 0x6C, 0xD1, 0x36, 0x10, 0x32, 0xCA, 0x9B, 0xD2, 0xAE, 0xB9, 0xD9, 0x00, 0xAA, 0x4D, 0x45, 0xD9, 0xEA, 0xD8, 0x0A, 0xC9, 0x42, 0x33, 0x74, 0xC4, 0x51, 0xA7, 0x25, 0x4D, 0x07, 0x66 }, + { { 0x03, 0x98, 0x37, 0x03, 0x75, 0xFC, 0xC9, 0xB6, 0xF1, 0x96, 0x17, 0xA5, 0x12, 0x87, 0x4A, 0xF6, 0x5B, 0xAD, 0x0F, 0xB8, 0x92, 0x60, 0x78, 0xB5, 0xF4, 0x0A, 0x1B, 0xEC, 0xBD, 0xA8, 0xE4, 0x20, 0x73 }, { 0x03, 0x02, 0xC7, 0x4C, 0xBB, 0xF9, 0x6E, 0x2E, 0x82, 0xB3, 0xB0, 0xE8, 0x7E, 0x65, 0x01, 0x16, 0x4B, 0xE9, 0x27, 0xE6, 0xC6, 0xEB, 0x00, 0x92, 0x8B, 0xA6, 0xFD, 0x6D, 0xBC, 0x10, 0x57, 0xB0, 0x42 } }, + { { 0xB3, 0xBD, 0x7A, 0x62, 0x81, 0xD4, 0x63, 0x56, 0x3F, 0x71, 0xBE, 0xDD, 0xE3, 0x75, 0xBA, 0x69, 0xCC, 0x39, 0x85, 0xBD, 0x4E, 0x41, 0x43, 0xAC, 0x4B, 0x96, 0x04, 0xFE, 0x80, 0xAC, 0x1D, 0x23 }, { 0x65, 0x78, 0xF2, 0xC3, 0x01, 0xA6, 0xC4, 0xAA, 0x7C, 0xE1, 0x7B, 0xB9, 0xC4, 0xE9, 0x72, 0xD2, 0xDB, 0xC2, 0x2C, 0x91, 0xEB, 0x37, 0xE5, 0x1A, 0xD5, 0x57, 0xA9, 0x6E, 0x2F, 0x1F, 0xF7, 0x03 } }, + { { 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10 }, { 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11 } }, + { 0x8C, 0x63, 0xA3, 0xAB, 0xFB, 0xEF, 0x2A, 0x76, 0x10, 0x31, 0x15, 0xB8, 0x53, 0xE6, 0x01, 0xD9, 0x4B, 0xD3, 0xE5, 0x2D, 0x1A, 0xC0, 0x8D, 0xA3, 0x5A, 0xEB, 0x3D, 0xD5, 0x49, 0x30, 0x06, 0x1C }, + { { 0x2D, 0xDA, 0x3A, 0xDD, 0x4B, 0x94, 0xF2, 0x56, 0x83, 0xD8, 0x0A, 0xD6, 0x71, 0xEF, 0xC6, 0xD5, 0xB7, 0xE8, 0x0F, 0x9F, 0x05, 0x86, 0x16, 0x87, 0xB3, 0xD2, 0x54, 0x60, 0x80, 0xFC, 0x67, 0x0E, 0x6A, 0xAA, 0xD0, 0x5D, 0xB0, 0xC2, 0x46, 0xFC, 0xFD, 0x44, 0x77, 0x6D, 0xC7, 0x24, 0xC4, 0x54, 0x25, 0x02, 0x9E, 0xB3, 0xBC, 0x88, 0xA2, 0x97, 0x34, 0xA6, 0x5E, 0xBC, 0x1E, 0x17, 0xE1, 0x2E }, { 0xE4, 0xAC, 0x35, 0x16, 0xEF, 0x35, 0x78, 0x3D, 0x15, 0x2A, 0x33, 0x9A, 0xF5, 0x50, 0x33, 0xED, 0x58, 0x1C, 0xC5, 0xA3, 0x41, 0xD2, 0x5D, 0x70, 0x51, 0x03, 0xD0, 0xE0, 0x6E, 0xF0, 0xBD, 0xE7, 0x4B, 0xBE, 0xA3, 0x32, 0x15, 0xBE, 0xD5, 0xC2, 0x61, 0x7A, 0x3F, 0x92, 0x59, 0x6C, 0x24, 0x8B, 0x3B, 0x29, 0xC0, 0x12, 0x7F, 0xD5, 0x51, 0xE0, 0x2F, 0x03, 0x2B, 0x6A, 0x1E, 0x6F, 0x27, 0x22 } }, + { 0x12, 0x86, 0x6F, 0xF4, 0x3A, 0xCA, 0x6A, 0x93, 0x99, 0x02, 0x3E, 0x71, 0x67, 0x3F, 0xFA, 0xC4, 0x55, 0x55, 0xF8, 0x5B, 0x98, 0x0F, 0xD3, 0xBC, 0x45, 0x03, 0xC6, 0xB4, 0x1F, 0xB6, 0xE3, 0xB4, 0xB6, 0x69, 0x73, 0x8F, 0xC6, 0x81, 0x1C, 0xBF, 0x5E, 0xBE, 0xB7, 0x00, 0x20, 0x90, 0xE8, 0xDF, 0x60, 0x2C, 0x5E, 0xC6, 0x3C, 0x5D, 0xF4, 0x77, 0x63, 0xA9, 0x8A, 0x26, 0x3C, 0x87, 0x08, 0x50 }, + { 0xC8, 0xEF, 0xE3, 0x84, 0x01, 0x4B, 0x87, 0x52, 0xF7, 0xC0, 0xF5, 0x71, 0x87, 0xD0, 0xE3, 0xA3, 0xB5, 0x82, 0x57, 0x21, 0xD4, 0x6D, 0xC8, 0x33, 0xA8, 0xAD, 0x50, 0xDA, 0x5C, 0x3D, 0xEC, 0x04 }, + { 0x03, 0xEC, 0x72, 0xD7, 0x1C, 0xD0, 0x9C, 0x26, 0xB2, 0xAE, 0x92, 0x82, 0x33, 0x86, 0x5E, 0x42, 0xD2, 0x63, 0x1F, 0xFB, 0x09, 0x17, 0xF1, 0x81, 0x67, 0x1D, 0x12, 0x09, 0xB1, 0xE8, 0xD1, 0xDC, 0x8F } + }, + { + 3, 2, 3, 3, + { 0, 1, 2 }, + { 0x02, 0x4D, 0x4B, 0x6C, 0xD1, 0x36, 0x10, 0x32, 0xCA, 0x9B, 0xD2, 0xAE, 0xB9, 0xD9, 0x00, 0xAA, 0x4D, 0x45, 0xD9, 0xEA, 0xD8, 0x0A, 0xC9, 0x42, 0x33, 0x74, 0xC4, 0x51, 0xA7, 0x25, 0x4D, 0x07, 0x66 }, + { { 0x03, 0x98, 0x37, 0x03, 0x75, 0xFC, 0xC9, 0xB6, 0xF1, 0x96, 0x17, 0xA5, 0x12, 0x87, 0x4A, 0xF6, 0x5B, 0xAD, 0x0F, 0xB8, 0x92, 0x60, 0x78, 0xB5, 0xF4, 0x0A, 0x1B, 0xEC, 0xBD, 0xA8, 0xE4, 0x20, 0x73 }, { 0x03, 0x02, 0xC7, 0x4C, 0xBB, 0xF9, 0x6E, 0x2E, 0x82, 0xB3, 0xB0, 0xE8, 0x7E, 0x65, 0x01, 0x16, 0x4B, 0xE9, 0x27, 0xE6, 0xC6, 0xEB, 0x00, 0x92, 0x8B, 0xA6, 0xFD, 0x6D, 0xBC, 0x10, 0x57, 0xB0, 0x42 }, { 0x03, 0x7A, 0x50, 0xEC, 0x9F, 0x4E, 0x35, 0x93, 0xD7, 0xDA, 0x1A, 0x7E, 0x7C, 0x53, 0xC3, 0x8C, 0xE9, 0x9E, 0xBF, 0x83, 0x9C, 0x92, 0xBA, 0x15, 0x8F, 0xFA, 0xAA, 0x26, 0x49, 0x0D, 0xEE, 0x11, 0x56 } }, + { { 0xB3, 0xBD, 0x7A, 0x62, 0x81, 0xD4, 0x63, 0x56, 0x3F, 0x71, 0xBE, 0xDD, 0xE3, 0x75, 0xBA, 0x69, 0xCC, 0x39, 0x85, 0xBD, 0x4E, 0x41, 0x43, 0xAC, 0x4B, 0x96, 0x04, 0xFE, 0x80, 0xAC, 0x1D, 0x23 }, { 0x65, 0x78, 0xF2, 0xC3, 0x01, 0xA6, 0xC4, 0xAA, 0x7C, 0xE1, 0x7B, 0xB9, 0xC4, 0xE9, 0x72, 0xD2, 0xDB, 0xC2, 0x2C, 0x91, 0xEB, 0x37, 0xE5, 0x1A, 0xD5, 0x57, 0xA9, 0x6E, 0x2F, 0x1F, 0xF7, 0x03 }, { 0x17, 0x34, 0x6B, 0x23, 0x81, 0x79, 0x25, 0xFE, 0xBA, 0x51, 0x38, 0x95, 0xA6, 0x5D, 0x2B, 0x3B, 0xEB, 0x4A, 0xD3, 0x66, 0x88, 0x2E, 0x86, 0x89, 0x5F, 0x19, 0x4D, 0xDD, 0xDD, 0x93, 0xD0, 0xE3 } }, + { { 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20 }, { 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21 }, { 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22 } }, + { 0x5B, 0x9F, 0x16, 0x0D, 0x52, 0x50, 0x7B, 0xA8, 0x74, 0x9A, 0xDE, 0x09, 0x21, 0xCB, 0xB9, 0x08, 0x4F, 0x27, 0xE5, 0x48, 0x56, 0x2F, 0xDC, 0x2B, 0x0E, 0x95, 0xDA, 0x0D, 0x43, 0x4F, 0x44, 0xB0 }, + { { 0x45, 0x84, 0xB1, 0x59, 0xF9, 0x51, 0x6B, 0xA6, 0x92, 0x07, 0xAD, 0x37, 0x67, 0x2C, 0x28, 0xCE, 0x8A, 0x8F, 0x32, 0x5E, 0x2F, 0xE3, 0x23, 0x9E, 0xD7, 0x92, 0x4D, 0x57, 0x41, 0xE3, 0x0C, 0x77, 0x2E, 0x3D, 0x93, 0x85, 0xA9, 0xE7, 0xA1, 0xD0, 0x57, 0xD9, 0x46, 0x26, 0x7D, 0x98, 0xA2, 0x54, 0x37, 0x10, 0xFD, 0x6F, 0xEF, 0x96, 0xD5, 0x9E, 0x1F, 0x9C, 0xA6, 0x44, 0x4E, 0xEF, 0xEF, 0x7E, 0x3F, 0xFB, 0x35, 0x82, 0xDE, 0x9B, 0x55, 0xDF, 0x55, 0x90, 0xCB, 0x7F, 0xFE, 0xB0, 0xEF, 0x47, 0x0A, 0x99, 0x55, 0xEF, 0x2E, 0xC7, 0x4A, 0x6F, 0x54, 0x67, 0x11, 0x62, 0xEF, 0xD9, 0x21, 0x2E }, { 0xD6, 0x9D, 0xAA, 0x79, 0x02, 0xF8, 0xE3, 0xED, 0x34, 0x76, 0xD9, 0x5D, 0xF3, 0xC2, 0x6A, 0xDA, 0xF4, 0x1A, 0xAE, 0x8E, 0x31, 0x4D, 0x4C, 0x48, 0xC7, 0xC4, 0xEC, 0x4C, 0x6A, 0xF6, 0x9A, 0xE7, 0x28, 0xEF, 0x39, 0xB0, 0x95, 0x21, 0xD2, 0x94, 0x76, 0x56, 0x02, 0x76, 0xED, 0x6E, 0xDA, 0xC7, 0xE8, 0x99, 0xEA, 0x1E, 0x70, 0x21, 0xAF, 0xBD, 0x7D, 0x45, 0x78, 0xC2, 0xC0, 0xDB, 0x7E, 0x10, 0xD0, 0x08, 0x43, 0x8D, 0x62, 0xF0, 0xFB, 0x7E, 0xDE, 0x8E, 0xB0, 0xFD, 0xD0, 0x12, 0x61, 0xE0, 0xC0, 0x11, 0x78, 0x51, 0xAA, 0xC3, 0x35, 0x5C, 0x7A, 0x65, 0xBA, 0x4C, 0xB7, 0x70, 0xC5, 0xC3 }, { 0x73, 0x7E, 0x87, 0x14, 0x73, 0x3B, 0x18, 0x64, 0x66, 0x80, 0x48, 0xDA, 0x16, 0xBC, 0x9B, 0x6D, 0x93, 0x29, 0xB1, 0x94, 0xCA, 0x08, 0x45, 0x70, 0x95, 0x6B, 0x33, 0x24, 0x74, 0xF3, 0x29, 0x46, 0x65, 0xFB, 0x89, 0xBD, 0x03, 0xB2, 0x51, 0xC6, 0x53, 0xBF, 0xE7, 0x70, 0x70, 0xD1, 0x33, 0xD8, 0xC2, 0x48, 0x5D, 0x67, 0xB8, 0xB4, 0x9B, 0x15, 0x4D, 0x49, 0x56, 0x27, 0xD5, 0x06, 0x1A, 0xD3, 0x6C, 0x23, 0x30, 0x99, 0x0D, 0x7E, 0x07, 0xD1, 0x74, 0xB3, 0x79, 0x76, 0x6B, 0x89, 0xB2, 0x6C, 0x27, 0x1D, 0x48, 0x1D, 0xC5, 0x17, 0x53, 0x51, 0xFA, 0x69, 0xBE, 0xDA, 0x1E, 0xF8, 0x6F, 0xD1 } }, + { 0x8F, 0xA0, 0xE2, 0xE7, 0x6F, 0x85, 0x67, 0xF8, 0x2C, 0xFE, 0xCF, 0x6F, 0x71, 0xAB, 0x2F, 0x18, 0x57, 0x24, 0xB5, 0x9A, 0x7B, 0xF0, 0x15, 0x1C, 0x74, 0xF0, 0x0E, 0x3B, 0x51, 0x96, 0x8F, 0x63, 0xBD, 0x28, 0x56, 0xF3, 0x42, 0xBB, 0xC6, 0x2B, 0x21, 0xEF, 0x30, 0x0D, 0xDB, 0xD8, 0xB0, 0xF4, 0xE1, 0xF3, 0x44, 0xF6, 0x18, 0x6D, 0x20, 0x70, 0xEA, 0x2B, 0x75, 0x2E, 0xE4, 0xD1, 0x88, 0x61, 0x7C, 0x26, 0xA9, 0xA9, 0x4F, 0x0A, 0x59, 0x2F, 0xA8, 0xD2, 0xF5, 0xF4, 0x3A, 0x4D, 0x03, 0x95, 0x37, 0x19, 0x39, 0x77, 0xEF, 0x59, 0x32, 0xE2, 0x09, 0x64, 0x2B, 0xFC, 0xF6, 0x0C, 0x15, 0x81 }, + { 0xC8, 0xEF, 0xE3, 0x84, 0x01, 0x4B, 0x87, 0x52, 0xF7, 0xC0, 0xF5, 0x71, 0x87, 0xD0, 0xE3, 0xA3, 0xB5, 0x82, 0x57, 0x21, 0xD4, 0x6D, 0xC8, 0x33, 0xA8, 0xAD, 0x50, 0xDA, 0x5C, 0x3D, 0xEC, 0x04 }, + { 0x03, 0xEC, 0x72, 0xD7, 0x1C, 0xD0, 0x9C, 0x26, 0xB2, 0xAE, 0x92, 0x82, 0x33, 0x86, 0x5E, 0x42, 0xD2, 0x63, 0x1F, 0xFB, 0x09, 0x17, 0xF1, 0x81, 0x67, 0x1D, 0x12, 0x09, 0xB1, 0xE8, 0xD1, 0xDC, 0x8F } + }, + { + 5, 3, 3, 2, + { 4, 0, 3 }, + { 0x03, 0x46, 0x0A, 0x7B, 0x96, 0x6E, 0xFF, 0xFB, 0x36, 0x94, 0x6F, 0x6D, 0xC3, 0xC1, 0x7F, 0xF7, 0x3A, 0xD7, 0x89, 0xFC, 0x1C, 0x2D, 0xF4, 0x3C, 0x12, 0x58, 0x03, 0x9C, 0xA2, 0xF9, 0xA1, 0xE3, 0xE6 }, + { { 0x02, 0x54, 0x3E, 0xAB, 0xF1, 0x39, 0x63, 0xF8, 0xE7, 0x71, 0x8B, 0x12, 0xE3, 0x7B, 0x83, 0x5C, 0x39, 0xE1, 0x81, 0xE2, 0x0D, 0xA6, 0x2F, 0xC0, 0xD5, 0x50, 0xF8, 0xF4, 0x19, 0xEE, 0x84, 0xD9, 0xB1 }, { 0x02, 0x13, 0x01, 0x9E, 0xAA, 0x53, 0x03, 0x4E, 0x29, 0x99, 0x8D, 0x82, 0x69, 0x3A, 0x0D, 0x11, 0x3E, 0xA4, 0x3F, 0xF4, 0xFF, 0x57, 0x5C, 0x13, 0x6C, 0x26, 0x19, 0x27, 0x89, 0xBD, 0xBA, 0xDE, 0x41 }, { 0x02, 0xA2, 0x22, 0xEB, 0x56, 0xCC, 0x64, 0xD2, 0x3D, 0x40, 0x14, 0x63, 0xDE, 0x34, 0x32, 0xE3, 0xFE, 0xF8, 0x30, 0x44, 0x18, 0xFE, 0x54, 0xF3, 0x6C, 0xF5, 0xC1, 0x21, 0x35, 0x7E, 0x4A, 0x4F, 0xAE } }, + { { 0xE9, 0x8D, 0xCB, 0xFA, 0xA9, 0x0D, 0xC4, 0xF5, 0x05, 0xB9, 0x07, 0xEF, 0x9A, 0xC6, 0xAB, 0x54, 0x9B, 0x78, 0xB0, 0xC7, 0xE6, 0x9B, 0x9B, 0xC2, 0x14, 0xF6, 0x01, 0x1F, 0xCD, 0xE6, 0x5A, 0xB5 }, { 0xAB, 0x57, 0x1E, 0x48, 0xD5, 0x38, 0x7E, 0x88, 0x19, 0x79, 0x39, 0xBB, 0x68, 0x8F, 0x81, 0x47, 0x20, 0xD2, 0x03, 0xCD, 0x47, 0x28, 0xEF, 0xD8, 0x5B, 0x27, 0xB7, 0x4F, 0xBF, 0xD5, 0xEB, 0x5C }, { 0x69, 0x77, 0x2F, 0xDC, 0x37, 0x09, 0x56, 0x48, 0xE3, 0xAE, 0x6A, 0x76, 0x89, 0xCA, 0x78, 0x0A, 0x44, 0x4F, 0xE4, 0x4C, 0xD1, 0xA4, 0xBE, 0xA7, 0x8B, 0x69, 0x28, 0x47, 0x1C, 0x55, 0x40, 0xB4 } }, + { { 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30 }, { 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31 }, { 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32 } }, + { 0x77, 0x95, 0x32, 0x9D, 0xC5, 0xF2, 0x78, 0x19, 0x9F, 0xD8, 0x64, 0xBB, 0xB4, 0x67, 0x72, 0x15, 0xC0, 0xB8, 0x29, 0xC7, 0x7D, 0x6C, 0x06, 0x56, 0x61, 0xCC, 0x30, 0xBD, 0xE3, 0x96, 0x91, 0x0B }, + { { 0xE6, 0x01, 0xBE, 0xBE, 0xDB, 0x3C, 0xEC, 0x62, 0x8C, 0x1F, 0x1E, 0xAB, 0x90, 0x05, 0x90, 0x2E, 0xE9, 0xC7, 0xC1, 0x28, 0x91, 0x95, 0x70, 0xBD, 0x0C, 0x9D, 0x2F, 0xBF, 0x40, 0xF1, 0x61, 0x2A, 0x9E, 0x57, 0xCE, 0xCA, 0x68, 0x2C, 0xD4, 0xC7, 0x06, 0xAE, 0x89, 0x9E, 0x67, 0xA1, 0x53, 0xB9, 0x88, 0xE3, 0xAE, 0x56, 0x1C, 0x95, 0xC3, 0xDE, 0x6F, 0x10, 0xEF, 0x3D, 0x9C, 0x7B, 0x66, 0x3A, 0x86, 0xDF, 0x8C, 0x79, 0x68, 0x0F, 0x5C, 0x5B, 0xEA, 0x55, 0xD3, 0xBE, 0x3A, 0xF5, 0xC6, 0x6A, 0x12, 0x4D, 0x60, 0x5E, 0x14, 0xBC, 0x8E, 0x18, 0xD9, 0x64, 0xCC, 0xD3, 0x44, 0x27, 0xAE, 0x64 }, { 0xF6, 0x03, 0x83, 0x3A, 0xBB, 0x2F, 0xA4, 0xE6, 0x7D, 0x0E, 0xEC, 0x30, 0xA7, 0x06, 0xA5, 0x4C, 0x36, 0xEB, 0x3D, 0x45, 0x4A, 0x6F, 0x2D, 0xF2, 0xF9, 0x55, 0x36, 0xDA, 0x38, 0xF2, 0xD5, 0xBA, 0x86, 0x82, 0x58, 0xBE, 0xD2, 0x6F, 0x58, 0xB9, 0xB0, 0x23, 0x76, 0x2F, 0xEA, 0x81, 0xD2, 0x1E, 0x67, 0x66, 0xB2, 0x49, 0xAA, 0xE7, 0xF2, 0x55, 0xC8, 0x60, 0x7F, 0x3D, 0x1F, 0x29, 0x87, 0xBC, 0xA0, 0x08, 0xA9, 0x12, 0x95, 0xEA, 0x6C, 0xCB, 0xD7, 0x0C, 0x7C, 0x93, 0xFF, 0xE4, 0xC8, 0xC9, 0x5C, 0x84, 0x20, 0x35, 0xF5, 0x16, 0x48, 0x28, 0x22, 0x75, 0xA5, 0x8F, 0x92, 0xF3, 0xCC, 0x46 }, { 0xBD, 0x14, 0x1C, 0x4E, 0xAB, 0x0A, 0x58, 0xD9, 0x6E, 0xB7, 0xDC, 0x5A, 0x77, 0x7B, 0x1D, 0x7B, 0xCB, 0x50, 0x04, 0x8F, 0x60, 0x4F, 0x06, 0xFD, 0xAE, 0xD6, 0xC7, 0x94, 0xFF, 0xCF, 0x6E, 0xF7, 0x57, 0xE3, 0xD4, 0x8B, 0xB0, 0xEA, 0x24, 0x19, 0xBE, 0x11, 0xBC, 0xB9, 0x3C, 0x0F, 0x03, 0xCA, 0x2E, 0x23, 0x77, 0x84, 0x61, 0x31, 0x32, 0xA3, 0xF0, 0xFF, 0x94, 0xC4, 0xD7, 0xEF, 0x0A, 0x5A, 0x22, 0x51, 0xA4, 0x4B, 0x42, 0xC2, 0xA0, 0xC3, 0x58, 0x1E, 0xF4, 0xDF, 0xAE, 0xD9, 0x29, 0x71, 0x88, 0xC0, 0xF2, 0x41, 0xCA, 0xE1, 0x2F, 0x65, 0x99, 0xEC, 0xC3, 0x0D, 0x83, 0x82, 0x08, 0x75 } }, + { 0x99, 0x19, 0x5E, 0x48, 0x41, 0x76, 0xEA, 0x22, 0x77, 0xE5, 0xE7, 0x36, 0xAE, 0x87, 0x52, 0xF9, 0x76, 0xA5, 0x49, 0x2F, 0xDD, 0xC2, 0x65, 0x36, 0x35, 0x24, 0x71, 0x14, 0xD9, 0x47, 0x23, 0x59, 0x7C, 0xBD, 0xFC, 0x14, 0xEB, 0x86, 0x51, 0x9A, 0x74, 0xE3, 0xBC, 0x87, 0x8E, 0x32, 0x29, 0xA3, 0x63, 0xBE, 0xFB, 0x3D, 0x79, 0x66, 0x48, 0x9C, 0x68, 0x9E, 0xA4, 0xB2, 0xC3, 0x5D, 0xB7, 0x0F, 0x49, 0x39, 0xD9, 0xD7, 0x40, 0xBC, 0x69, 0xEB, 0x19, 0x81, 0x45, 0x31, 0xE9, 0xB3, 0xB8, 0xA6, 0x3C, 0xE3, 0x95, 0xEF, 0x25, 0x6B, 0x65, 0x6A, 0xD5, 0xF4, 0xD6, 0xE3, 0x8A, 0x67, 0x41, 0xDE }, + { 0x5F, 0x11, 0x34, 0x34, 0x6D, 0xB9, 0xA5, 0xA8, 0x06, 0x4A, 0xE8, 0xF0, 0x26, 0x6D, 0x35, 0x44, 0x5C, 0x98, 0xFD, 0x75, 0xCD, 0x4B, 0x73, 0x01, 0xB3, 0xE5, 0x8E, 0x1E, 0x56, 0xD5, 0xDB, 0x05 }, + { 0x02, 0x47, 0xE5, 0x9F, 0xCF, 0x15, 0x5E, 0xCB, 0x62, 0x47, 0xDE, 0x52, 0xB7, 0x57, 0x59, 0x1A, 0x3E, 0x3B, 0xF8, 0xEB, 0x2B, 0x57, 0x24, 0x4B, 0xF5, 0x84, 0xF9, 0x1B, 0x52, 0x15, 0xCE, 0x7C, 0x8C } + }, + { + 6, 4, 4, 6, + { 0, 2, 3, 5 }, + { 0x03, 0x80, 0x03, 0x89, 0x51, 0xDF, 0x18, 0x6D, 0x2F, 0x43, 0x9A, 0xA6, 0x33, 0xC3, 0x32, 0xF3, 0x0F, 0x1D, 0x78, 0xDE, 0x48, 0xE0, 0x9D, 0xDD, 0xFB, 0xB4, 0x2F, 0x61, 0x18, 0xBC, 0xC9, 0x11, 0x70 }, + { { 0x02, 0x97, 0xF2, 0xDD, 0x64, 0x80, 0xC8, 0x04, 0x44, 0x5E, 0x26, 0x60, 0x56, 0x90, 0x77, 0xA5, 0xDD, 0x3C, 0xBF, 0x77, 0xBB, 0x10, 0x0F, 0x3A, 0xBA, 0x93, 0xC0, 0xF1, 0xE3, 0x02, 0xF9, 0xFA, 0xCD }, { 0x02, 0xFD, 0x49, 0xCD, 0x57, 0xE2, 0xB0, 0x7B, 0x1E, 0x6A, 0xEE, 0x83, 0x37, 0x90, 0xDF, 0x69, 0x94, 0x53, 0x94, 0x7C, 0xF4, 0xE3, 0xBE, 0x64, 0x7D, 0x5B, 0x9C, 0x83, 0x7D, 0x0D, 0xEA, 0x18, 0x10 }, { 0x02, 0xCC, 0xE4, 0xE5, 0xDD, 0x62, 0x4E, 0x42, 0x08, 0x38, 0xC0, 0x92, 0xF1, 0x93, 0xEC, 0x47, 0x42, 0xEA, 0xA4, 0xAE, 0xA5, 0xDF, 0x89, 0x14, 0x11, 0x9F, 0xCB, 0x53, 0xED, 0x9A, 0x0B, 0xA1, 0xD9 }, { 0x02, 0xBF, 0x4C, 0xEF, 0xF9, 0x81, 0x62, 0x6E, 0x40, 0xAD, 0x77, 0x41, 0xFE, 0x22, 0xC2, 0x97, 0xA3, 0xDB, 0x90, 0x71, 0xCD, 0x7D, 0x94, 0x1D, 0x74, 0xE8, 0xB3, 0x86, 0xA5, 0x4F, 0xC5, 0x55, 0x2F } }, + { { 0x82, 0x1E, 0x21, 0x58, 0xE3, 0xA4, 0x8C, 0x53, 0x1B, 0xF7, 0xCF, 0x24, 0xC1, 0x8C, 0x44, 0xA0, 0x7F, 0x37, 0x12, 0xB0, 0x90, 0xF7, 0xAB, 0xC8, 0xF5, 0xC8, 0xDA, 0xFB, 0x83, 0xAB, 0x56, 0xCF }, { 0x5B, 0x8E, 0xEC, 0x15, 0xEA, 0x88, 0x54, 0xD9, 0x75, 0x0B, 0x4F, 0x92, 0xD2, 0xE6, 0x7D, 0x79, 0x1E, 0x5B, 0xE6, 0x4B, 0x67, 0xAC, 0x7C, 0xAB, 0x73, 0xBD, 0xAA, 0x0F, 0x43, 0x3D, 0x62, 0x97 }, { 0xFB, 0x5A, 0x8F, 0x7F, 0xC6, 0x2C, 0x78, 0xAD, 0x59, 0x9C, 0x5C, 0x4C, 0x88, 0xD8, 0xF4, 0xD1, 0x2E, 0x75, 0xA1, 0x78, 0x6A, 0xA3, 0xAF, 0xA5, 0x18, 0x84, 0xC7, 0xFA, 0x1E, 0x98, 0x44, 0xCC }, { 0xAB, 0x71, 0x43, 0x4D, 0xA5, 0x55, 0x57, 0xB2, 0xB6, 0xF2, 0x48, 0x88, 0xCE, 0x79, 0x0B, 0xB0, 0x1D, 0x37, 0x00, 0x50, 0x32, 0x53, 0x8B, 0xC8, 0x20, 0xC1, 0xF6, 0xFD, 0xFB, 0x4E, 0x80, 0x93 } }, + { { 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40 }, { 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41 }, { 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42 }, { 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43 } }, + { 0x38, 0xD6, 0x35, 0x27, 0x21, 0x01, 0x29, 0x3F, 0xB4, 0xC8, 0x11, 0x7B, 0x06, 0xDB, 0x92, 0xD3, 0x49, 0x42, 0xB6, 0x4E, 0x6B, 0x8C, 0x07, 0x39, 0xF3, 0xB7, 0x5B, 0xA9, 0x02, 0x22, 0xF2, 0x73 }, + { { 0x6B, 0xE2, 0x93, 0xC2, 0xD3, 0x28, 0x76, 0x27, 0xED, 0x0C, 0xF2, 0x23, 0x1F, 0xC4, 0x59, 0x0F, 0xA0, 0x80, 0xC2, 0xF6, 0x81, 0xAE, 0x96, 0x22, 0x3C, 0x2C, 0x00, 0x86, 0x07, 0xF1, 0x1B, 0x61, 0x6F, 0xD1, 0x3D, 0xC9, 0x55, 0xED, 0xAB, 0x3D, 0x71, 0x17, 0x03, 0x44, 0x05, 0x75, 0x87, 0x3A, 0x86, 0x15, 0xA5, 0xDD, 0xEF, 0xB6, 0x31, 0xF2, 0x38, 0x15, 0xAA, 0xE7, 0x87, 0x3D, 0xF8, 0x1B, 0x42, 0x7D, 0x4D, 0xB1, 0xBC, 0x4E, 0x96, 0x97, 0x23, 0xAE, 0xDE, 0xB8, 0xEF, 0x22, 0x52, 0x46, 0x55, 0x9C, 0x89, 0xD3, 0xD0, 0xEA, 0x38, 0x4C, 0x50, 0x9A, 0x43, 0x41, 0x37, 0x41, 0x99, 0x23, 0xAD, 0xC2, 0xD3, 0x6B, 0x59, 0x26, 0x43, 0x15, 0x72, 0xFD, 0x3F, 0x6A, 0xD1, 0x6B, 0xB1, 0xF9, 0x93, 0x14, 0xBF, 0xAB, 0x48, 0xAB, 0xFB, 0x5F, 0xF2, 0x12, 0x10, 0x6C, 0xA5, 0x50, 0xE6, 0x5D }, { 0x4F, 0x76, 0x7A, 0x5A, 0xD9, 0xE5, 0xA3, 0xA4, 0xBA, 0x36, 0xAF, 0x19, 0xB9, 0xDB, 0xB0, 0xF2, 0xFA, 0x8E, 0xB1, 0x13, 0xE2, 0x38, 0x56, 0x16, 0xF5, 0xCD, 0xE6, 0xB3, 0x53, 0xEB, 0x96, 0xE0, 0x74, 0xAE, 0xAD, 0x17, 0xB8, 0xD3, 0x33, 0xCD, 0xF7, 0xA8, 0xDB, 0xA7, 0x21, 0xDE, 0x62, 0x09, 0xA4, 0x23, 0xF9, 0xA6, 0xC4, 0x53, 0xEE, 0x46, 0x6B, 0xC3, 0x6B, 0x15, 0xAC, 0x62, 0x59, 0xCB, 0x9D, 0x56, 0xC9, 0x53, 0x6A, 0x4D, 0xBE, 0xF2, 0x40, 0x98, 0x8D, 0x46, 0xC7, 0xD4, 0x2E, 0x90, 0x22, 0x88, 0x82, 0x0F, 0x1D, 0x94, 0xC4, 0xA5, 0xD8, 0xE3, 0x0D, 0xE0, 0x58, 0x76, 0x92, 0xE1, 0xB1, 0x30, 0xD3, 0x7B, 0xC2, 0x92, 0x68, 0x27, 0x6C, 0xA9, 0xD6, 0xB0, 0xD5, 0x25, 0x36, 0xDD, 0x54, 0x87, 0x62, 0xFF, 0x22, 0x2D, 0x0D, 0x3A, 0xE0, 0x96, 0xFD, 0x11, 0x41, 0x29, 0xE5, 0x7A }, { 0x67, 0x9E, 0xAD, 0xD5, 0x31, 0x32, 0xA6, 0x30, 0xBA, 0x82, 0xAD, 0xFC, 0x2F, 0x9C, 0xDE, 0x31, 0x39, 0xED, 0xD0, 0x92, 0x78, 0x9D, 0x2C, 0xE0, 0x47, 0xEE, 0xEF, 0x91, 0x75, 0x8C, 0x95, 0x58, 0x27, 0xEF, 0x16, 0xB7, 0x70, 0x5A, 0x78, 0xB3, 0xD7, 0x82, 0xD4, 0xF0, 0x7B, 0x79, 0x9D, 0x0B, 0x7A, 0x2B, 0x3F, 0xE3, 0x09, 0xED, 0x0F, 0x1F, 0x02, 0xE7, 0x81, 0x76, 0x18, 0x8F, 0xC1, 0x8D, 0xA9, 0xC5, 0x3B, 0x51, 0x44, 0x2B, 0x63, 0x1D, 0x74, 0x31, 0x29, 0x8A, 0xD4, 0x1A, 0x30, 0xCC, 0x3B, 0xC3, 0x7B, 0x2B, 0xD0, 0x43, 0xEE, 0xFD, 0x29, 0xD9, 0x22, 0x82, 0x9F, 0x7E, 0x5C, 0xBE, 0xD9, 0x42, 0xC2, 0x23, 0x01, 0x95, 0x9B, 0x48, 0x93, 0x57, 0xE2, 0x56, 0x5D, 0x6B, 0x80, 0xAA, 0xB6, 0x66, 0x1B, 0xE5, 0x1E, 0x56, 0xD7, 0xD5, 0xA8, 0x2B, 0x83, 0xDA, 0x39, 0x49, 0xC0, 0xB3 }, { 0xA7, 0xE3, 0x05, 0xC0, 0x28, 0x61, 0xA2, 0xA6, 0xAB, 0x80, 0xEC, 0x58, 0x54, 0x5E, 0xDF, 0xDC, 0x3A, 0xB6, 0xA0, 0x81, 0xCA, 0xDF, 0x0A, 0xC2, 0xEE, 0x3E, 0xF0, 0x92, 0xD8, 0x6D, 0x2B, 0x04, 0x87, 0xEE, 0xF7, 0xD7, 0x98, 0xD3, 0xEC, 0xEE, 0xDA, 0x68, 0x89, 0x44, 0x56, 0x45, 0x03, 0xFE, 0xCD, 0xAD, 0x08, 0xFD, 0xEC, 0x7C, 0x43, 0xF2, 0xBF, 0xCF, 0x41, 0x57, 0xDA, 0x2F, 0x37, 0x58, 0xE3, 0x89, 0xA3, 0x5E, 0x82, 0x07, 0x41, 0xB3, 0xAC, 0x9E, 0x60, 0xE7, 0x84, 0x15, 0x6F, 0x14, 0x9F, 0x4E, 0x50, 0xC9, 0x6F, 0x44, 0xE8, 0x95, 0x0E, 0xCA, 0x4F, 0x59, 0xAF, 0x8B, 0xEE, 0x42, 0xEE, 0x81, 0x00, 0x90, 0xE3, 0x29, 0x9A, 0xCA, 0x1E, 0x24, 0x0A, 0xC4, 0x27, 0x35, 0xFC, 0x4E, 0xD6, 0xFA, 0x08, 0x87, 0x14, 0x5A, 0xC5, 0x4F, 0x9E, 0x52, 0xBA, 0x48, 0xB6, 0x13, 0x3F, 0x51 } }, + { 0xCA, 0xDA, 0xC1, 0xB3, 0x06, 0xA2, 0x62, 0xA4, 0x0D, 0x47, 0x3B, 0x91, 0x5D, 0x9B, 0xC8, 0x11, 0x55, 0x05, 0x08, 0x37, 0xF8, 0x1A, 0x83, 0xA0, 0xA8, 0x55, 0x68, 0xD0, 0xD9, 0xA0, 0x31, 0x5C, 0x94, 0x5D, 0xF9, 0x70, 0x17, 0xEF, 0x44, 0xAE, 0x1A, 0xAB, 0x3D, 0x1F, 0xF9, 0x12, 0x8A, 0x4F, 0xB7, 0x63, 0x0B, 0x7E, 0xFB, 0x2A, 0xD3, 0x0E, 0xA6, 0xBD, 0x7A, 0x3E, 0x56, 0x29, 0x09, 0x8A, 0x6D, 0x22, 0xF5, 0xB4, 0xEC, 0xCE, 0xFA, 0x5A, 0x85, 0x16, 0xF6, 0x72, 0x0F, 0x26, 0x20, 0xB9, 0xDD, 0xD9, 0x1E, 0x0A, 0xCF, 0x76, 0x94, 0x0C, 0xE2, 0x7C, 0x05, 0xE4, 0x3E, 0x55, 0xF4, 0x82, 0x26, 0xB7, 0x69, 0x9B, 0x00, 0x77, 0xE1, 0x4F, 0x91, 0x23, 0x03, 0x36, 0x2B, 0x32, 0x65, 0xD4, 0x44, 0xEF, 0xB0, 0x62, 0x8F, 0xB0, 0xC5, 0x0C, 0xD9, 0xB0, 0x2F, 0xFA, 0x65, 0x35, 0x08, 0x18 }, + { 0xF3, 0x13, 0x1A, 0x73, 0x0B, 0xD8, 0x82, 0xFC, 0x3E, 0x2C, 0x72, 0x59, 0x91, 0x06, 0xD8, 0xF0, 0x74, 0x82, 0x05, 0x3D, 0xA3, 0x24, 0x0F, 0x8D, 0x4B, 0x6C, 0xBA, 0x61, 0x03, 0x1D, 0xF6, 0x3F }, + { 0x03, 0xE1, 0xBF, 0xC6, 0x86, 0xB3, 0x60, 0x2C, 0xF7, 0xD2, 0x8A, 0x47, 0x77, 0x4A, 0x9E, 0x54, 0x67, 0xD5, 0xC7, 0x82, 0x71, 0x90, 0xAA, 0xA5, 0x4A, 0xB1, 0xB2, 0xB4, 0x8A, 0xF2, 0x00, 0x8B, 0x4E } + }, +}; + +#endif /* SECP256K1_MODULE_FROST_ENROLLMENT_VECTORS_H */ diff --git a/tools/test_vectors_frost_enrollment_generate.py b/tools/test_vectors_frost_enrollment_generate.py new file mode 100755 index 00000000..bf8adbe0 --- /dev/null +++ b/tools/test_vectors_frost_enrollment_generate.py @@ -0,0 +1,359 @@ +#!/usr/bin/env python3 + +"""Generates src/modules/frost_enrollment/vectors.h. + +These are REGRESSION vectors, not cross-validation vectors. FROST enrollment +has no BIP and therefore no published test vectors, and the reference proof of +concept (https://github.com/siv2r/frost-enrollment) draws its randomness from +secrets.randbits, which is not seedable -- so there is nothing to check the C +implementation against. This script therefore re-implements the same math +independently, in plain Python, and freezes the result. + +What that buys is real but bounded: it pins the two tag strings, the exact +parameters hash serialization, the share-splitting derivation and the +identifier conventions, so that any change to them is a loud, deliberate, +vector-breaking change rather than a silent one. It is NOT evidence that the +protocol is implemented correctly -- the algebraic invariants in +src/modules/frost_enrollment/tests_impl.h are what carry that. + +The one thing this file does establish independently is the group arithmetic: +the elliptic curve operations below are written from the secp256k1 parameters +rather than borrowed from the library, so a vector mismatch in the derived +public share or the threshold key really is a disagreement between two +implementations. + +Usage: %s > src/modules/frost_enrollment/vectors.h +""" + +import hashlib +import sys +import textwrap + +# secp256k1 domain parameters. +P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F +ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 +GX = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798 +GY = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8 +G = (GX, GY) + +MAX_PARTICIPANTS = 128 + + +# --- group arithmetic (points are (x, y) or None for infinity) --- + + +def point_add(a, b): + if a is None: + return b + if b is None: + return a + if a[0] == b[0] and (a[1] + b[1]) % P == 0: + return None + if a == b: + lam = 3 * a[0] * a[0] * pow(2 * a[1], P - 2, P) % P + else: + lam = (b[1] - a[1]) * pow(b[0] - a[0], P - 2, P) % P + x = (lam * lam - a[0] - b[0]) % P + return (x, (lam * (a[0] - x) - a[1]) % P) + + +def point_mul(point, scalar): + result = None + scalar %= ORDER + while scalar: + if scalar & 1: + result = point_add(result, point) + point = point_add(point, point) + scalar >>= 1 + return result + + +def cbytes(point): + """33-byte compressed serialization.""" + assert point is not None + return bytes([2 + (point[1] & 1)]) + point[0].to_bytes(32, "big") + + +# --- hashing --- + + +def tagged_hash(tag, msg): + tag_hash = hashlib.sha256(tag.encode()).digest() + return hashlib.sha256(tag_hash + tag_hash + msg).digest() + + +def ser32(x): + return x.to_bytes(4, "big") + + +# --- the protocol, mirroring src/modules/frost_enrollment/enrollment_impl.h --- + + +def params_hash(thresh_pk, ids, new_id, n_participants, threshold): + msg = cbytes(thresh_pk) + msg += ser32(n_participants) + ser32(threshold) + ser32(new_id) + msg += ser32(len(ids)) + for i in sorted(ids): + msg += ser32(i) + return tagged_hash("FROST enrollment/params_hash", msg) + + +def lagrange_at(ids, my_id, new_id): + """The Lagrange basis polynomial of my_id over ids, at the target. + + Identifier space: the x-coordinate of identifier id is id + 1, so an + x-coordinate difference is an identifier difference and the +1 cancels.""" + num, deno = 1, 1 + for other in ids: + if other == my_id: + continue + num = num * (new_id - other) % ORDER + deno = deno * (my_id - other) % ORDER + return num * pow(deno, ORDER - 2, ORDER) % ORDER + + +def derive_mask(rand32, ph32, my_id, recipient_id): + msg = rand32 + ph32 + ser32(my_id) + ser32(recipient_id) + # from_bytes_wrapping: reduce mod the group order rather than reject. + return int.from_bytes(tagged_hash("FROST enrollment/share_split", msg), "big") % ORDER + + +def shares_gen(secshare, thresh_pk, ids, my_id, new_id, n_participants, threshold, secrand32): + ph32 = params_hash(thresh_pk, ids, new_id, n_participants, threshold) + v = lagrange_at(ids, my_id, new_id) * secshare % ORDER + rand32 = bytes( + a ^ b + for a, b in zip( + tagged_hash("FROST enrollment/share_split", secrand32), + secshare.to_bytes(32, "big"), + ) + ) + out = [0] * len(ids) + my_pos = ids.index(my_id) + for j, recipient in enumerate(ids): + if j == my_pos: + continue + out[j] = derive_mask(rand32, ph32, my_id, recipient) + v = (v - out[j]) % ORDER + out[my_pos] = v + return out, ph32 + + +def trusted_dealer_keygen(thresh_sk, n_participants, threshold): + """The frost module's trusted dealer (src/modules/frost/keygen_impl.h).""" + coeffs = [] + for i in range(1, threshold): + h = tagged_hash("BIP0445/trusted/keygen", thresh_sk.to_bytes(32, "big") + ser32(i)) + c = int.from_bytes(h, "big") + assert 0 < c < ORDER + coeffs.append(c) + secshares = [] + for i in range(n_participants): + x = i + 1 + share = 0 + for c in coeffs: + share = (share * x + c) % ORDER + share = (share * x + thresh_sk) % ORDER + assert share != 0 + secshares.append(share) + return secshares, point_mul(G, thresh_sk), [point_mul(G, s) for s in secshares] + + +def run_case(thresh_sk, n_participants, threshold, ids, new_id, seeds): + secshares, thresh_pk, pubshares = trusted_dealer_keygen(thresh_sk, n_participants, threshold) + ph32 = params_hash(thresh_pk, ids, new_id, n_participants, threshold) + + shares = [] + for k, my_id in enumerate(ids): + out, ph = shares_gen( + secshares[my_id], thresh_pk, ids, my_id, new_id, n_participants, threshold, seeds[k] + ) + assert ph == ph32 + shares.append(out) + + # Round 1.2: helper j sums entry j of every helper's output. + sigmas = [sum(shares[i][j] for i in range(len(ids))) % ORDER for j in range(len(ids))] + + # Round 2, and the independent check that the result really is f(x_new). + new_secshare = sum(sigmas) % ORDER + expected = sum(lagrange_at(ids, i, new_id) * secshares[i] for i in ids) % ORDER + assert new_secshare == expected + new_pubshare = None + for i in ids: + new_pubshare = point_add(new_pubshare, point_mul(pubshares[i], lagrange_at(ids, i, new_id))) + assert new_pubshare == point_mul(G, new_secshare) + + return { + "n_participants": n_participants, + "threshold": threshold, + "ids": ids, + "new_id": new_id, + "thresh_pk": thresh_pk, + "pubshares": [pubshares[i] for i in ids], + "secshares": [secshares[i] for i in ids], + "seeds": seeds, + "params_hash": ph32, + "shares": shares, + "sigmas": sigmas, + "new_secshare": new_secshare, + "new_pubshare": new_pubshare, + } + + +# --- C emission --- + + +def byte_array(b): + return "{ %s }" % ", ".join("0x%02X" % x for x in b) + + +def scalar_array(x): + return byte_array(x.to_bytes(32, "big")) + + +def indent(s, level=1): + return textwrap.indent(s, 4 * level * " ") + + +def emit_case(c): + n_ids = len(c["ids"]) + lines = [] + lines.append("%d, %d, %d, %d," % (c["n_participants"], c["threshold"], n_ids, c["new_id"])) + lines.append("{ %s }," % ", ".join(str(i) for i in c["ids"])) + lines.append("%s," % byte_array(cbytes(c["thresh_pk"]))) + lines.append("{ %s }," % ", ".join(byte_array(cbytes(p)) for p in c["pubshares"])) + lines.append("{ %s }," % ", ".join(scalar_array(s) for s in c["secshares"])) + lines.append("{ %s }," % ", ".join(byte_array(s) for s in c["seeds"])) + lines.append("%s," % byte_array(c["params_hash"])) + lines.append( + "{ %s }," + % ", ".join( + "{ %s }" % ", ".join("0x%02X" % b for s in row for b in s.to_bytes(32, "big")) + for row in c["shares"] + ) + ) + lines.append( + "{ %s }," + % ", ".join("0x%02X" % b for s in c["sigmas"] for b in s.to_bytes(32, "big")) + ) + lines.append("%s," % scalar_array(c["new_secshare"])) + lines.append("%s" % byte_array(cbytes(c["new_pubshare"]))) + return "{\n" + indent("\n".join(lines)) + "\n}," + + +# Fixed inputs. Nothing here is random at run time: the whole point is that +# regenerating this file without an intentional change reproduces it byte for +# byte. +CASES = [ + # A 2-of-3 group enrolling a fourth participant with the minimum helper + # set. The base case, and the one the module documentation walks through. + # This key has EVEN Y; the three below have odd Y. Nothing in enrollment + # depends on the parity of the threshold key -- unlike frost signing, it + # never takes an x-only view of it -- so this is coverage rather than a + # distinction the code makes. + dict( + thresh_sk=0x0202020202020202020202020202020202020202020202020202020202020202, + n_participants=3, + threshold=2, + ids=[0, 1], + new_id=3, + seeds=[bytes([0x10 + i] * 32) for i in range(2)], + ), + # The same group with an oversized helper set: u = 3 > t = 2. The resulting + # share must be the one the u = 2 case produces, which the C test checks + # separately; here it is simply frozen. + dict( + thresh_sk=0x0202020202020202020202020202020202020202020202020202020202020202, + n_participants=3, + threshold=2, + ids=[0, 1, 2], + new_id=3, + seeds=[bytes([0x20 + i] * 32) for i in range(3)], + ), + # Repair: a 3-of-5 group reproducing participant 2's lost share. The helper + # set is deliberately unsorted, to pin that the parameters hash + # canonicalizes identifiers while the share buffers follow the caller's + # order. + dict( + thresh_sk=0x02030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F2021, + n_participants=5, + threshold=3, + ids=[4, 0, 3], + new_id=2, + seeds=[bytes([0x30 + i] * 32) for i in range(3)], + ), + # A larger enrollment, 4-of-6 to 4-of-7, and the only case whose DERIVED + # public share has odd Y. + dict( + thresh_sk=0x1122334455667788990011223344556677889900112233445566778899001122, + n_participants=6, + threshold=4, + ids=[0, 2, 3, 5], + new_id=6, + seeds=[bytes([0x40 + i] * 32) for i in range(4)], + ), +] + + +def main(): + cases = [run_case(**c) for c in CASES] + max_ids = max(len(c["ids"]) for c in cases) + + out = """/** + * Automatically generated by tools/test_vectors_frost_enrollment_generate.py. + * + * REGRESSION vectors, not cross-validation vectors. FROST enrollment has no + * BIP and no published test vectors, and the reference proof of concept + * (https://github.com/siv2r/frost-enrollment) draws its randomness from + * secrets.randbits, which is not seedable, so there is nothing to check + * against. The generator re-implements the math independently in Python and + * freezes the result. + * + * What these pin: the two tag strings ("FROST enrollment/params_hash" and + * "FROST enrollment/share_split"), the parameters hash serialization, the + * share-splitting derivation, and the identifier conventions. Changing any of + * them is a vector-breaking change. What they do NOT establish is protocol + * correctness -- the algebraic invariants in tests_impl.h carry that. + * + * Used by the tests in src/modules/frost_enrollment/tests_impl.h. */ + +#ifndef SECP256K1_MODULE_FROST_ENROLLMENT_VECTORS_H +#define SECP256K1_MODULE_FROST_ENROLLMENT_VECTORS_H + +#define FROST_ENROLLMENT_VEC_MAX_IDS %d + +struct frost_enrollment_vec_case { + /* Parameters. */ + size_t n_participants; + uint32_t threshold; + size_t n_ids; + uint32_t new_id; + uint32_t ids[FROST_ENROLLMENT_VEC_MAX_IDS]; + /* Group key material, aligned with ids. */ + unsigned char thresh_pk33[33]; + unsigned char pubshares33[FROST_ENROLLMENT_VEC_MAX_IDS][33]; + unsigned char secshares32[FROST_ENROLLMENT_VEC_MAX_IDS][32]; + /* Round 1.1 inputs and outputs. shares32[i] is helper ids[i]'s output + * buffer, aligned with ids. */ + unsigned char session_secrand32[FROST_ENROLLMENT_VEC_MAX_IDS][32]; + unsigned char params_hash32[32]; + unsigned char shares32[FROST_ENROLLMENT_VEC_MAX_IDS][FROST_ENROLLMENT_VEC_MAX_IDS * 32]; + /* Round 1.2 and round 2 outputs. */ + unsigned char sigmas32[FROST_ENROLLMENT_VEC_MAX_IDS * 32]; + unsigned char new_secshare32[32]; + unsigned char new_pubshare33[33]; +}; + +static const struct frost_enrollment_vec_case frost_enrollment_vec_cases[%d] = { +""" % ( + max_ids, + len(cases), + ) + for c in cases: + out += indent(emit_case(c)) + "\n" + out += "};\n\n#endif /* SECP256K1_MODULE_FROST_ENROLLMENT_VECTORS_H */\n" + sys.stdout.write(out) + + +if __name__ == "__main__": + main()