chilldkg: Phase 6 - test vectors, FROST integration, docs, example
Final phase of the ChillDKG module: upstream test vectors, a DKG->FROST integration test, boundary tests, full module documentation and a runnable example. Test vectors: - tools/test_vectors_chilldkg_generate.py converts all 10 upstream bip-frost-dkg JSON vector files into src/modules/chilldkg/vectors.h (modeled on tools/test_vectors_frost_generate.py; takes the vectors directory as an argument; upstream pinned to commit a91896883f85b159415ecf298d5e844879af112d, recorded in the generated header with the exact regeneration invocation; regeneration is reproducible byte-for-byte). - tests_impl.h vector runners execute 191 of 241 upstream cases through the public API: hostpubkey_gen, params_hash, participant_step1/step2/finalize/investigate, coordinator_step1/finalize/investigate, recover. Happy paths are byte-exact (pmsg1/cmsg1/pmsg2/cmsg2/dkg_output/recovery/cinv); error cases assert both the fault enum and fault_index against expectedError.participantId. The 50 skipped cases are wrong-length/wrong-count inputs not expressible with the fixed-size C API; each skip is documented in vectors.h. Boundary/robustness tests: t=1, t=n, n=2, a full n=128/t=2 session end-to-end with per-participant secshare*G == pubshare checks and a recovery roundtrip, and a state1 memcpy roundtrip (step2 from a copied state object). DKG->FROST integration test (guarded by ENABLE_MODULE_FROST): a full ChillDKG session (n=3, t=2) feeds (secshare, thresh_pk, pubshares) directly into the frost module. ChillDKG's thresh_pk is already TapTweak'ed, so frost_tweak_cache_init is called with no further tweaks (frost's tweaked x-only key asserted equal to the x-only part of the ChillDKG thresh_pk); signers 0 and 2 run nonce_gen, nonce_agg, session_init with the shared x = id+1 convention, frost_sign, partial_sig_verify and partial_sig_agg; the aggregate signature verifies as a plain BIP-340 signature against the threshold key. Example: examples/chilldkg.c runs a full 2-of-3 DKG session (host key generation, params hash, participant/coordinator steps, finalize, and a recovery roundtrip via participant_recover) with fixed-size buffers and secret erasure. Wired into Makefile.am and examples/CMakeLists.txt exactly like frost_example (runs as a TEST); chilldkg_example binary added to .gitignore. Docs: src/modules/chilldkg/chilldkg.md now documents the protocol summary, message-flow table with exact byte sizes, blame taxonomy, recovery workflow, security notes (host key reuse/retention, fresh randomness per session, state secrecy, recovery-data sensitivity) and the pinned reference commit; src/modules/frost/frost.md points at the new module as the intended DKG. Bug fix found by the vector runner (recover tcId 9): the internal recover() passed a possibly-NULL fault_index from coordinator_recover to certeq_verify, which dereferences it on failure; now uses a local. Verified: make check 10/10 (3 test suites + 7 examples incl. chilldkg_example, exit 0 when run); CMake ctest 428/428 with chilldkg + frost, and a no-frost build confirms the ENABLE_MODULE_FROST guard; make distdir includes vectors.h, the example and the generator. The module is feature-complete against bip-frost-dkg v0.3.0-dev at a91896883f85b159415ecf298d5e844879af112d. The BIP is still a draft; tagged hashes and wire formats may change upstream.
This commit is contained in:
@@ -35,3 +35,7 @@ endif()
|
||||
if(SECP256K1_ENABLE_MODULE_FROST)
|
||||
add_example(frost)
|
||||
endif()
|
||||
|
||||
if(SECP256K1_ENABLE_MODULE_CHILLDKG)
|
||||
add_example(chilldkg)
|
||||
endif()
|
||||
|
||||
212
examples/chilldkg.c
Normal file
212
examples/chilldkg.c
Normal file
@@ -0,0 +1,212 @@
|
||||
/*************************************************************************
|
||||
* To the extent possible under law, the author(s) have dedicated all *
|
||||
* copyright and related and neighboring rights to the software in this *
|
||||
* file to the public domain worldwide. This software is distributed *
|
||||
* without any warranty. For the CC0 Public Domain Dedication, see *
|
||||
* EXAMPLES_COPYING or https://creativecommons.org/publicdomain/zero/1.0 *
|
||||
*************************************************************************/
|
||||
|
||||
/** This file demonstrates how to use the ChillDKG module to run a 2-of-3
|
||||
* distributed key generation (DKG) session for FROST (BIP 445) threshold
|
||||
* signatures. Additionally, see the documentation in
|
||||
* include/secp256k1_chilldkg.h and src/modules/chilldkg/chilldkg.md.
|
||||
*
|
||||
* The example runs all roles (participants and coordinator) in a single
|
||||
* process. In a real deployment these roles are performed by different
|
||||
* parties communicating over secure/authenticated channels, and the
|
||||
* coordinator is untrusted.
|
||||
*
|
||||
* The resulting key material (secret share, threshold public key and public
|
||||
* shares) can be used with the FROST signing module (see examples/frost.c).
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <secp256k1.h>
|
||||
#include <secp256k1_chilldkg.h>
|
||||
|
||||
#include "examples_util.h"
|
||||
|
||||
/* Total number of participants n */
|
||||
#define N_PARTICIPANTS 3
|
||||
/* Threshold t: the minimum number of signers required to produce a
|
||||
* signature */
|
||||
#define THRESHOLD 2
|
||||
|
||||
struct participant {
|
||||
/* Long-term secret key; the participant's identity. All session outputs
|
||||
* can be recovered from the hostseckey and the recovery data. */
|
||||
unsigned char hostseckey[32];
|
||||
unsigned char hostpubkey[33];
|
||||
secp256k1_chilldkg_participant_state1 state1;
|
||||
secp256k1_chilldkg_participant_state2 state2;
|
||||
/* Buffers for the messages sent to the coordinator */
|
||||
unsigned char pmsg1[33 * THRESHOLD + 32 * N_PARTICIPANTS + 97];
|
||||
unsigned char pmsg2[64];
|
||||
/* Outputs */
|
||||
unsigned char secshare[32];
|
||||
unsigned char recovery[4 + 33 * THRESHOLD + 162 * N_PARTICIPANTS];
|
||||
};
|
||||
|
||||
int main(void) {
|
||||
secp256k1_context *ctx;
|
||||
struct participant participants[N_PARTICIPANTS];
|
||||
secp256k1_chilldkg_coordinator_state coord_state;
|
||||
unsigned char hostpubkeys[N_PARTICIPANTS * 33];
|
||||
unsigned char params_hash[32];
|
||||
unsigned char cmsg1[162 * N_PARTICIPANTS + 33 * (THRESHOLD - 1)];
|
||||
unsigned char cmsg2[64 * N_PARTICIPANTS];
|
||||
const unsigned char *pmsg1_ptrs[N_PARTICIPANTS];
|
||||
const unsigned char *pmsg2_ptrs[N_PARTICIPANTS];
|
||||
unsigned char thresh_pk[33];
|
||||
unsigned char pubshares[N_PARTICIPANTS * 33];
|
||||
unsigned char coord_recovery[sizeof(participants[0].recovery)];
|
||||
uint32_t fault_index;
|
||||
secp256k1_chilldkg_fault fault;
|
||||
int i;
|
||||
|
||||
ctx = secp256k1_context_create(SECP256K1_CONTEXT_NONE);
|
||||
|
||||
/* Every participant generates a long-term host key pair. The hostseckey
|
||||
* must be generated with a cryptographically secure random number
|
||||
* generator and stored securely. */
|
||||
printf("Generating host keys...\n");
|
||||
for (i = 0; i < N_PARTICIPANTS; i++) {
|
||||
if (!fill_random(participants[i].hostseckey, 32)) {
|
||||
printf("Failed to generate randomness\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (!secp256k1_chilldkg_hostpubkey_gen(ctx, participants[i].hostpubkey, participants[i].hostseckey)) {
|
||||
printf("Failed to generate host public key\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
memcpy(&hostpubkeys[33 * i], participants[i].hostpubkey, 33);
|
||||
}
|
||||
|
||||
/* The participants exchange their host public keys (over authenticated
|
||||
* channels) and compare the parameters hash out of band to ensure they
|
||||
* all agree on the session parameters. */
|
||||
if (!secp256k1_chilldkg_params_hash(ctx, params_hash, hostpubkeys, N_PARTICIPANTS, THRESHOLD)) {
|
||||
printf("Invalid session parameters\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
printf("Session parameters hash: ");
|
||||
print_hex(params_hash, 32);
|
||||
|
||||
/* Step 1: every participant creates its first message and sends it to the
|
||||
* coordinator. The randomness must be FRESH for every session. */
|
||||
printf("Running participant step 1...\n");
|
||||
for (i = 0; i < N_PARTICIPANTS; i++) {
|
||||
unsigned char random32[32];
|
||||
if (!fill_random(random32, 32)) {
|
||||
printf("Failed to generate randomness\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (!secp256k1_chilldkg_participant_step1(ctx, &participants[i].state1, participants[i].pmsg1, participants[i].hostseckey, hostpubkeys, N_PARTICIPANTS, THRESHOLD, random32)) {
|
||||
printf("participant_step1 failed\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
secure_erase(random32, sizeof(random32));
|
||||
pmsg1_ptrs[i] = participants[i].pmsg1;
|
||||
}
|
||||
|
||||
/* Step 1 (coordinator): the coordinator aggregates the first messages and
|
||||
* broadcasts cmsg1 to all participants. */
|
||||
printf("Running coordinator step 1...\n");
|
||||
fault = secp256k1_chilldkg_coordinator_step1(ctx, &coord_state, cmsg1, &fault_index, pmsg1_ptrs, hostpubkeys, N_PARTICIPANTS, THRESHOLD);
|
||||
if (fault != SECP256K1_CHILLDKG_OK) {
|
||||
printf("coordinator_step1 failed (fault %d, index %u)\n", fault, fault_index);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
/* Step 2: every participant verifies cmsg1, computes its DKG output, and
|
||||
* sends a CertEq signature over the session transcript to the
|
||||
* coordinator. */
|
||||
printf("Running participant step 2...\n");
|
||||
for (i = 0; i < N_PARTICIPANTS; i++) {
|
||||
unsigned char aux_rand32[32];
|
||||
if (!fill_random(aux_rand32, 32)) {
|
||||
printf("Failed to generate randomness\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
fault = secp256k1_chilldkg_participant_step2(ctx, &participants[i].state2, participants[i].pmsg2, &fault_index, NULL, &participants[i].state1, participants[i].hostseckey, cmsg1, aux_rand32);
|
||||
secure_erase(aux_rand32, sizeof(aux_rand32));
|
||||
if (fault != SECP256K1_CHILLDKG_OK) {
|
||||
printf("participant_step2 failed for participant %d (fault %d, index %u)\n", i, fault, fault_index);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
pmsg2_ptrs[i] = participants[i].pmsg2;
|
||||
}
|
||||
|
||||
/* Step 2 (coordinator): the coordinator collects the CertEq signatures
|
||||
* into the certificate and broadcasts it to all participants. */
|
||||
printf("Running coordinator finalize...\n");
|
||||
fault = secp256k1_chilldkg_coordinator_finalize(ctx, cmsg2, thresh_pk, pubshares, coord_recovery, &fault_index, &coord_state, pmsg2_ptrs);
|
||||
if (fault != SECP256K1_CHILLDKG_OK) {
|
||||
printf("coordinator_finalize failed (fault %d, index %u)\n", fault, fault_index);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
/* Finalize: every participant verifies the certificate and outputs the
|
||||
* DKG result and the recovery data. */
|
||||
printf("Running participant finalize...\n");
|
||||
for (i = 0; i < N_PARTICIPANTS; i++) {
|
||||
fault = secp256k1_chilldkg_participant_finalize(ctx, participants[i].secshare, thresh_pk, pubshares, participants[i].recovery, &fault_index, &participants[i].state2, cmsg2);
|
||||
if (fault != SECP256K1_CHILLDKG_OK) {
|
||||
printf("participant_finalize failed for participant %d (fault %d, index %u)\n", i, fault, fault_index);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
/* All participants (and the coordinator) hold identical recovery
|
||||
* data. Keep it safe: anyone with the recovery data and their
|
||||
* hostseckey can recover the DKG output, e.g. after data loss. */
|
||||
if (memcmp(participants[i].recovery, coord_recovery, sizeof(coord_recovery)) != 0) {
|
||||
printf("recovery data mismatch\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
printf("Threshold public key: ");
|
||||
print_hex(thresh_pk, 33);
|
||||
for (i = 0; i < N_PARTICIPANTS; i++) {
|
||||
printf("Public share of participant %d: ", i);
|
||||
print_hex(&pubshares[33 * i], 33);
|
||||
}
|
||||
|
||||
/* Recovery: a participant can recover its DKG output from a backup of its
|
||||
* hostseckey and the recovery data at any time. */
|
||||
printf("Recovering participant 0 from recovery data...\n");
|
||||
{
|
||||
unsigned char rec_secshare[32];
|
||||
unsigned char rec_thresh_pk[33];
|
||||
unsigned char rec_pubshares[N_PARTICIPANTS * 33];
|
||||
unsigned char rec_hostpubkeys[N_PARTICIPANTS * 33];
|
||||
size_t n_rec;
|
||||
uint32_t t_rec;
|
||||
fault = secp256k1_chilldkg_participant_recover(ctx, rec_secshare, rec_thresh_pk, rec_pubshares, rec_hostpubkeys, &n_rec, &t_rec, &fault_index, participants[0].hostseckey, participants[0].recovery, sizeof(participants[0].recovery));
|
||||
if (fault != SECP256K1_CHILLDKG_OK) {
|
||||
printf("participant_recover failed (fault %d, index %u)\n", fault, fault_index);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (n_rec != N_PARTICIPANTS || t_rec != THRESHOLD
|
||||
|| memcmp(rec_secshare, participants[0].secshare, 32) != 0
|
||||
|| memcmp(rec_thresh_pk, thresh_pk, 33) != 0) {
|
||||
printf("recovered output mismatch\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
secure_erase(rec_secshare, sizeof(rec_secshare));
|
||||
}
|
||||
|
||||
printf("DKG session completed successfully.\n");
|
||||
|
||||
/* Clear secrets from memory (see examples/frost.c for why). The
|
||||
* participants' state2 objects have been consumed by
|
||||
* participant_finalize. */
|
||||
for (i = 0; i < N_PARTICIPANTS; i++) {
|
||||
secure_erase(participants[i].hostseckey, 32);
|
||||
secure_erase(participants[i].secshare, 32);
|
||||
}
|
||||
secp256k1_context_destroy(ctx);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
Reference in New Issue
Block a user