chilldkg: Phase 1 - internal primitives (util, vss)
Add the byte-exact internal primitives for the ChillDKG module,
mirroring the Python reference implementation of the bip-frost-dkg
draft (v0.3.0-dev), pinned to upstream commit
a91896883f85b159415ecf298d5e844879af112d.
util.h / util_impl.h (mirrors chilldkg_ref/util.py):
- Point (de)serialization with explicit point-at-infinity support:
33 zero bytes <-> infinity, otherwise SEC compressed. Checked parse
rejects invalid encodings and out-of-range x coordinates
(point_save/point_load, xonly_save/xonly_load).
- Internal parameterized-tag BIP-340 Schnorr sign/verify
(chilldkg_schnorrsig_sign/_verify): tag prefix selects the
<prefix>/aux, /nonce, /challenge subtags ("BIP DKG/pop message" for
proofs of possession, "BIP0340" for CertEq signatures and recovery
acks), arbitrary-length messages, pad33 zero-padding helper. The
public schnorrsig API hardcodes BIP0340/32-byte messages, so the
algorithm is replicated from secp256k1_schnorrsig_sign_internal with
a custom tag; cross-checked against secp256k1_schnorrsig_sign32.
- Tagged hashes via secp256k1_sha256_initialize_tagged:
"BIP DKG/params_hash", "BIP DKG/encpedpop seed",
"BIP DKG/simplpedpop aux", "BIP DKG/encpedpop secnonce",
"BIP DKG/encpedpop ecdh", "BIP DKG/encaps_multi self_pad",
"BIP DKG/vss coeffs", and BIP-341 "TapTweak" (32-byte x-only input).
- params_hash = TH("BIP DKG/params_hash", u32be(t) || hostpubkeys)
(note: plan had the operand order reversed; the reference hashes t
first).
- ECDH pads: reuses the ecdh module's SHA256-of-compressed-shared-
point hash, then TH("BIP DKG/encpedpop ecdh", ecdh ||
sender_pubnonce || receiver_hostpubkey || context) with a sending
flag fixing the sender|receiver order; self_pad for the own index.
Pads are parsed wrapping (mod-n reduction); wire scalars, VSS
coefficients and the TapTweak are parsed checked.
vss.h / vss_impl.h (mirrors chilldkg_ref/vss.py):
- vss_gen_coeffs: per-coefficient TH("BIP DKG/vss coeffs", seed ||
u32be(j)), checked parse with bitwise error accumulation.
- vss_poly_eval (Horner) and vss_secshare_for with the x = id+1
convention (safe at UINT32_MAX).
- vss_commit (constant-time ecmult_gen, zero coefficient -> infinity),
vss_pubshare (powers-of-x over commitments, skips infinity),
vss_commitment_add, vss_verify_secshare.
- vss_invalid_taproot_commit: TapTweak applied to the x-only constant
term so the Taproot script path is unspendable; returns tweak and
pubtweak.
tests_impl.h: 7 vector tests (tagged hashes, params_hash, point
serialization incl. infinity roundtrip and parity prefixes, checked
vs wrapping scalar parse at the group order boundary, custom-tag
schnorrsig incl. wrong-tag/key/msg rejection, ECDH pad sender/receiver
symmetry, VSS coeff derivation/Horner/commitment/pubshare/tweak) with
expected values generated once from the Python reference
(committed into the test file, reference commit recorded).
Verified: make check 3/3 suites pass; CMake ctest all pass;
./tests --target=chilldkg runs all 7 new tests green in both verify
and noverify builds.
2026-08-31 04:05:15 +02:00
|
|
|
/***********************************************************************
|
|
|
|
|
* Distributed under the MIT software license, see the accompanying *
|
|
|
|
|
* file COPYING or https://www.opensource.org/licenses/mit-license.php.*
|
|
|
|
|
***********************************************************************/
|
|
|
|
|
|
|
|
|
|
#ifndef SECP256K1_MODULE_CHILLDKG_UTIL_IMPL_H
|
|
|
|
|
#define SECP256K1_MODULE_CHILLDKG_UTIL_IMPL_H
|
|
|
|
|
|
|
|
|
|
#include <string.h>
|
|
|
|
|
|
|
|
|
|
#include "../../../include/secp256k1.h"
|
|
|
|
|
|
|
|
|
|
#include "util.h"
|
|
|
|
|
#include "../../ecmult.h"
|
|
|
|
|
#include "../../ecmult_const.h"
|
|
|
|
|
#include "../../util.h"
|
|
|
|
|
|
|
|
|
|
static void secp256k1_chilldkg_sha256_tagged_params_hash(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha) {
|
|
|
|
|
secp256k1_sha256_initialize_tagged(hash_ctx, sha, (const unsigned char *)"BIP DKG/params_hash", sizeof("BIP DKG/params_hash") - 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void secp256k1_chilldkg_sha256_tagged_encpedpop_seed(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha) {
|
|
|
|
|
secp256k1_sha256_initialize_tagged(hash_ctx, sha, (const unsigned char *)"BIP DKG/encpedpop seed", sizeof("BIP DKG/encpedpop seed") - 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void secp256k1_chilldkg_sha256_tagged_simplpedpop_aux(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha) {
|
|
|
|
|
secp256k1_sha256_initialize_tagged(hash_ctx, sha, (const unsigned char *)"BIP DKG/simplpedpop aux", sizeof("BIP DKG/simplpedpop aux") - 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void secp256k1_chilldkg_sha256_tagged_encpedpop_secnonce(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha) {
|
|
|
|
|
secp256k1_sha256_initialize_tagged(hash_ctx, sha, (const unsigned char *)"BIP DKG/encpedpop secnonce", sizeof("BIP DKG/encpedpop secnonce") - 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void secp256k1_chilldkg_sha256_tagged_encpedpop_ecdh(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha) {
|
|
|
|
|
secp256k1_sha256_initialize_tagged(hash_ctx, sha, (const unsigned char *)"BIP DKG/encpedpop ecdh", sizeof("BIP DKG/encpedpop ecdh") - 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void secp256k1_chilldkg_sha256_tagged_self_pad(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha) {
|
|
|
|
|
secp256k1_sha256_initialize_tagged(hash_ctx, sha, (const unsigned char *)"BIP DKG/encaps_multi self_pad", sizeof("BIP DKG/encaps_multi self_pad") - 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void secp256k1_chilldkg_sha256_tagged_vss_coeffs(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha) {
|
|
|
|
|
secp256k1_sha256_initialize_tagged(hash_ctx, sha, (const unsigned char *)"BIP DKG/vss coeffs", sizeof("BIP DKG/vss coeffs") - 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void secp256k1_chilldkg_sha256_tagged_taptweak(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha) {
|
|
|
|
|
secp256k1_sha256_initialize_tagged(hash_ctx, sha, (const unsigned char *)"TapTweak", sizeof("TapTweak") - 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void secp256k1_chilldkg_point_save(unsigned char *out33, const secp256k1_ge *p) {
|
|
|
|
|
if (secp256k1_ge_is_infinity(p)) {
|
|
|
|
|
memset(out33, 0, 33);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
{
|
|
|
|
|
secp256k1_ge tmp = *p;
|
|
|
|
|
/* Serialization operates on public data (commitments, pubnonces, host
|
|
|
|
|
* public keys), so variable-time normalization is fine. */
|
|
|
|
|
secp256k1_fe_normalize_var(&tmp.x);
|
|
|
|
|
secp256k1_fe_normalize_var(&tmp.y);
|
|
|
|
|
out33[0] = secp256k1_fe_is_odd(&tmp.y) ? 0x03 : 0x02;
|
|
|
|
|
secp256k1_fe_get_b32(&out33[1], &tmp.x);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int secp256k1_chilldkg_point_load(secp256k1_ge *p, const unsigned char *in33) {
|
|
|
|
|
static const unsigned char zeros33[33] = { 0 };
|
|
|
|
|
secp256k1_fe x;
|
|
|
|
|
|
|
|
|
|
/* Parsed data comes from protocol messages, i.e., it is public. */
|
|
|
|
|
if (secp256k1_memcmp_var(in33, zeros33, 33) == 0) {
|
|
|
|
|
secp256k1_ge_set_infinity(p);
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
if (in33[0] != 0x02 && in33[0] != 0x03) {
|
|
|
|
|
secp256k1_ge_set_infinity(p);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
if (!secp256k1_fe_set_b32_limit(&x, &in33[1])) {
|
|
|
|
|
secp256k1_ge_set_infinity(p);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
if (!secp256k1_ge_set_xo_var(p, &x, in33[0] == 0x03)) {
|
|
|
|
|
secp256k1_ge_set_infinity(p);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void secp256k1_chilldkg_xonly_save(unsigned char *out32, const secp256k1_ge *p) {
|
|
|
|
|
secp256k1_ge tmp = *p;
|
|
|
|
|
VERIFY_CHECK(!secp256k1_ge_is_infinity(p));
|
|
|
|
|
secp256k1_fe_normalize_var(&tmp.x);
|
|
|
|
|
secp256k1_fe_get_b32(out32, &tmp.x);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int secp256k1_chilldkg_xonly_load(secp256k1_ge *p, const unsigned char *in32) {
|
|
|
|
|
secp256k1_fe x;
|
|
|
|
|
if (!secp256k1_fe_set_b32_limit(&x, in32)) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
return secp256k1_ge_set_xo_var(p, &x, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void secp256k1_chilldkg_pad33(unsigned char *out33, const char *str) {
|
|
|
|
|
size_t len = strlen(str);
|
|
|
|
|
VERIFY_CHECK(len <= 33);
|
|
|
|
|
memcpy(out33, str, len);
|
|
|
|
|
memset(out33 + len, 0, 33 - len);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Initializes sha with the BIP 340 tagged hash tag tag_prefix || subtag,
|
|
|
|
|
* e.g., "BIP DKG/pop message" || "/nonce". Tag strings are public constants. */
|
|
|
|
|
static void secp256k1_chilldkg_schnorrsig_sha256_tagged(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha, const char *tag_prefix, const char *subtag) {
|
|
|
|
|
unsigned char tag[64];
|
|
|
|
|
size_t prefix_len = strlen(tag_prefix);
|
|
|
|
|
size_t subtag_len = strlen(subtag);
|
|
|
|
|
|
|
|
|
|
VERIFY_CHECK(prefix_len + subtag_len <= sizeof(tag));
|
|
|
|
|
memcpy(tag, tag_prefix, prefix_len);
|
|
|
|
|
memcpy(tag + prefix_len, subtag, subtag_len);
|
|
|
|
|
secp256k1_sha256_initialize_tagged(hash_ctx, sha, tag, prefix_len + subtag_len);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* BIP 340 nonce derivation with parameterized tag prefix, mirroring
|
|
|
|
|
* schnorr_sign in secp256k1lab/bip340.py:
|
|
|
|
|
* t = seckey32 XOR TH(tag_prefix || "/aux", aux_rand32)
|
|
|
|
|
* nonce32 = TH(tag_prefix || "/nonce", t || xonly_pk32 || msg) */
|
|
|
|
|
static void secp256k1_chilldkg_schnorrsig_nonce(const secp256k1_hash_ctx *hash_ctx, unsigned char *nonce32, const unsigned char *msg, size_t msglen, const unsigned char *seckey32, const unsigned char *xonly_pk32, const unsigned char *aux_rand32, const char *tag_prefix) {
|
|
|
|
|
secp256k1_sha256 sha;
|
|
|
|
|
unsigned char masked_key[32];
|
|
|
|
|
unsigned char rand[32];
|
|
|
|
|
int i;
|
|
|
|
|
|
|
|
|
|
secp256k1_chilldkg_schnorrsig_sha256_tagged(hash_ctx, &sha, tag_prefix, "/aux");
|
|
|
|
|
secp256k1_sha256_write(hash_ctx, &sha, aux_rand32, 32);
|
|
|
|
|
secp256k1_sha256_finalize(hash_ctx, &sha, rand);
|
|
|
|
|
for (i = 0; i < 32; i++) {
|
|
|
|
|
masked_key[i] = seckey32[i] ^ rand[i];
|
|
|
|
|
}
|
|
|
|
|
secp256k1_chilldkg_schnorrsig_sha256_tagged(hash_ctx, &sha, tag_prefix, "/nonce");
|
|
|
|
|
secp256k1_sha256_write(hash_ctx, &sha, masked_key, 32);
|
|
|
|
|
secp256k1_sha256_write(hash_ctx, &sha, xonly_pk32, 32);
|
|
|
|
|
secp256k1_sha256_write(hash_ctx, &sha, msg, msglen);
|
|
|
|
|
secp256k1_sha256_finalize(hash_ctx, &sha, nonce32);
|
|
|
|
|
secp256k1_sha256_clear(&sha);
|
|
|
|
|
secp256k1_memclear_explicit(masked_key, sizeof(masked_key));
|
|
|
|
|
secp256k1_memclear_explicit(rand, sizeof(rand));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* BIP 340 challenge hash with parameterized tag prefix:
|
|
|
|
|
* e = TH(tag_prefix || "/challenge", r32 || pubkey32 || msg) mod n
|
|
|
|
|
* The reduction modulo the group order matches the reference, which reduces
|
|
|
|
|
* the hash output with int_from_bytes(...) % GE.ORDER. */
|
|
|
|
|
static void secp256k1_chilldkg_schnorrsig_challenge(const secp256k1_hash_ctx *hash_ctx, secp256k1_scalar *e, const unsigned char *r32, const unsigned char *msg, size_t msglen, const unsigned char *pubkey32, const char *tag_prefix) {
|
|
|
|
|
unsigned char buf[32];
|
|
|
|
|
secp256k1_sha256 sha;
|
|
|
|
|
|
|
|
|
|
secp256k1_chilldkg_schnorrsig_sha256_tagged(hash_ctx, &sha, tag_prefix, "/challenge");
|
|
|
|
|
secp256k1_sha256_write(hash_ctx, &sha, r32, 32);
|
|
|
|
|
secp256k1_sha256_write(hash_ctx, &sha, pubkey32, 32);
|
|
|
|
|
secp256k1_sha256_write(hash_ctx, &sha, msg, msglen);
|
|
|
|
|
secp256k1_sha256_finalize(hash_ctx, &sha, buf);
|
|
|
|
|
secp256k1_scalar_set_b32(e, buf, NULL);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Mirrors schnorr_sign in secp256k1lab/bip340.py. The structure follows
|
|
|
|
|
* secp256k1_schnorrsig_sign_internal, which cannot be reused directly because
|
|
|
|
|
* it hardcodes the "BIP0340" tags. */
|
|
|
|
|
static int secp256k1_chilldkg_schnorrsig_sign(const secp256k1_context *ctx, unsigned char *sig64, const unsigned char *msg, size_t msglen, const unsigned char *seckey32, const unsigned char *aux_rand32, const char *tag_prefix) {
|
|
|
|
|
const secp256k1_hash_ctx *hash_ctx;
|
|
|
|
|
secp256k1_scalar sk;
|
|
|
|
|
secp256k1_scalar e;
|
|
|
|
|
secp256k1_scalar k;
|
|
|
|
|
secp256k1_ge pk;
|
|
|
|
|
secp256k1_ge r;
|
|
|
|
|
unsigned char nonce32[32] = { 0 };
|
|
|
|
|
unsigned char pk32[32];
|
|
|
|
|
unsigned char seckey[32];
|
|
|
|
|
int overflow;
|
|
|
|
|
int ret = 1;
|
|
|
|
|
|
|
|
|
|
VERIFY_CHECK(ctx != NULL);
|
|
|
|
|
ARG_CHECK(secp256k1_ecmult_gen_context_is_built(&ctx->ecmult_gen_ctx));
|
|
|
|
|
ARG_CHECK(sig64 != NULL);
|
|
|
|
|
ARG_CHECK(msg != NULL || msglen == 0);
|
|
|
|
|
ARG_CHECK(seckey32 != NULL);
|
|
|
|
|
ARG_CHECK(aux_rand32 != NULL);
|
|
|
|
|
ARG_CHECK(tag_prefix != NULL);
|
|
|
|
|
hash_ctx = secp256k1_get_hash_context(ctx);
|
|
|
|
|
|
|
|
|
|
secp256k1_scalar_set_b32(&sk, seckey32, &overflow);
|
|
|
|
|
overflow |= secp256k1_scalar_is_zero(&sk);
|
|
|
|
|
/* Branching on the validity of the secret key is fine: whether the
|
|
|
|
|
* caller's secret key is in range 1..n-1 is not secret. */
|
|
|
|
|
secp256k1_declassify(ctx, &overflow, sizeof(overflow));
|
|
|
|
|
if (overflow) {
|
|
|
|
|
memset(sig64, 0, 64);
|
|
|
|
|
secp256k1_scalar_clear(&sk);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
secp256k1_ecmult_gen_ge(&ctx->ecmult_gen_ctx, &pk, &sk);
|
|
|
|
|
/* The public key is not secret, so variable-time normalization and
|
|
|
|
|
* branching on its y parity are fine. */
|
chilldkg: CI wiring, ctime_tests coverage, declassify fixes
CI:
- ci/ci.sh: new CHILLDKG environment variable, passed to configure as
--enable-module-chilldkg (mirroring FROST).
- .github/workflows/ci.yml: default CHILLDKG: 'no' and CHILLDKG: 'yes'
in every job that enables FROST, except the x86_64 matrix entry that
deliberately builds without the ecdh module (chilldkg requires
schnorrsig + ecdh; the configure-time dependency error would fire
there). YAML validity and per-job dependency presence checked
programmatically.
ctime_tests:
- src/ctime_tests.c: run a full ChillDKG session (n = 2, t = 2) through
the public API under the memory checker: hostpubkey_gen, params_hash,
participant_step1, coordinator_step1, participant_step2,
coordinator_finalize, participant_finalize, participant_recover and
recovery_ack_sign. Host secret keys, session randomness, aux
randomness and the resulting secret shares are undefined (secret);
all protocol messages, the certificate, threshold public key, public
shares, recovery data, ack signature and the secret-free state1
objects are defined (public). state2 stays secret (contains the
secret share).
Constant-time fixes found by running the new block under
MemorySanitizer (valgrind unavailable locally; MSan build via clang +
CMake). All are missing declassifications of secret-derived but public
(or public-outcome) values, following the frost module's
secp256k1_declassify pattern with justification comments; no real
constant-time bugs were found:
- hostpubkey_gen: declassify the computed host public key before
serialization (public output).
- participant_step1: declassify the zero-randomness check result (only
reveals "the RNG returned 32 zero bytes", which aborts the session).
- encpedpop participant_step1: declassify the pubnonce point before
serialization (public, part of pmsg1).
- chilldkg_schnorrsig_sign: declassify the signer public key before
normalization/parity branch, and declassify the return value (a
failure only reveals a zero derived nonce, negligible probability).
- vss_commit: declassify the VSS commitments before serialization
(public, part of pmsg1).
- vss_verify_secshare: declassify secshare*G before the infinity/eq
checks (equals the public pubshare in honest runs; the discrete log
is not revealed).
- simplpedpop_participant_investigate (proactive audit; not reached by
ctime_tests): declassify the secshare-sum comparison result (the
public fault code reveals it anyway).
Verified: MSan ctime_tests exits 0; autotools make check 10/10 (the
local tree is configured without --enable-ctime-tests because neither
valgrind nor an MSan-instrumented gcc build is available; CI runs
ctime_tests under valgrind as before); CMake ctest 428/428;
./tests --target=chilldkg and ./chilldkg_example pass.
2026-08-31 10:25:14 +02:00
|
|
|
secp256k1_declassify(ctx, &pk, sizeof(pk));
|
chilldkg: Phase 1 - internal primitives (util, vss)
Add the byte-exact internal primitives for the ChillDKG module,
mirroring the Python reference implementation of the bip-frost-dkg
draft (v0.3.0-dev), pinned to upstream commit
a91896883f85b159415ecf298d5e844879af112d.
util.h / util_impl.h (mirrors chilldkg_ref/util.py):
- Point (de)serialization with explicit point-at-infinity support:
33 zero bytes <-> infinity, otherwise SEC compressed. Checked parse
rejects invalid encodings and out-of-range x coordinates
(point_save/point_load, xonly_save/xonly_load).
- Internal parameterized-tag BIP-340 Schnorr sign/verify
(chilldkg_schnorrsig_sign/_verify): tag prefix selects the
<prefix>/aux, /nonce, /challenge subtags ("BIP DKG/pop message" for
proofs of possession, "BIP0340" for CertEq signatures and recovery
acks), arbitrary-length messages, pad33 zero-padding helper. The
public schnorrsig API hardcodes BIP0340/32-byte messages, so the
algorithm is replicated from secp256k1_schnorrsig_sign_internal with
a custom tag; cross-checked against secp256k1_schnorrsig_sign32.
- Tagged hashes via secp256k1_sha256_initialize_tagged:
"BIP DKG/params_hash", "BIP DKG/encpedpop seed",
"BIP DKG/simplpedpop aux", "BIP DKG/encpedpop secnonce",
"BIP DKG/encpedpop ecdh", "BIP DKG/encaps_multi self_pad",
"BIP DKG/vss coeffs", and BIP-341 "TapTweak" (32-byte x-only input).
- params_hash = TH("BIP DKG/params_hash", u32be(t) || hostpubkeys)
(note: plan had the operand order reversed; the reference hashes t
first).
- ECDH pads: reuses the ecdh module's SHA256-of-compressed-shared-
point hash, then TH("BIP DKG/encpedpop ecdh", ecdh ||
sender_pubnonce || receiver_hostpubkey || context) with a sending
flag fixing the sender|receiver order; self_pad for the own index.
Pads are parsed wrapping (mod-n reduction); wire scalars, VSS
coefficients and the TapTweak are parsed checked.
vss.h / vss_impl.h (mirrors chilldkg_ref/vss.py):
- vss_gen_coeffs: per-coefficient TH("BIP DKG/vss coeffs", seed ||
u32be(j)), checked parse with bitwise error accumulation.
- vss_poly_eval (Horner) and vss_secshare_for with the x = id+1
convention (safe at UINT32_MAX).
- vss_commit (constant-time ecmult_gen, zero coefficient -> infinity),
vss_pubshare (powers-of-x over commitments, skips infinity),
vss_commitment_add, vss_verify_secshare.
- vss_invalid_taproot_commit: TapTweak applied to the x-only constant
term so the Taproot script path is unspendable; returns tweak and
pubtweak.
tests_impl.h: 7 vector tests (tagged hashes, params_hash, point
serialization incl. infinity roundtrip and parity prefixes, checked
vs wrapping scalar parse at the group order boundary, custom-tag
schnorrsig incl. wrong-tag/key/msg rejection, ECDH pad sender/receiver
symmetry, VSS coeff derivation/Horner/commitment/pubshare/tweak) with
expected values generated once from the Python reference
(committed into the test file, reference commit recorded).
Verified: make check 3/3 suites pass; CMake ctest all pass;
./tests --target=chilldkg runs all 7 new tests green in both verify
and noverify builds.
2026-08-31 04:05:15 +02:00
|
|
|
secp256k1_fe_normalize_var(&pk.x);
|
|
|
|
|
secp256k1_fe_normalize_var(&pk.y);
|
|
|
|
|
if (secp256k1_fe_is_odd(&pk.y)) {
|
|
|
|
|
secp256k1_scalar_negate(&sk, &sk);
|
|
|
|
|
}
|
|
|
|
|
secp256k1_scalar_get_b32(seckey, &sk);
|
|
|
|
|
secp256k1_fe_get_b32(pk32, &pk.x);
|
|
|
|
|
|
|
|
|
|
secp256k1_chilldkg_schnorrsig_nonce(hash_ctx, nonce32, msg, msglen, seckey, pk32, aux_rand32, tag_prefix);
|
|
|
|
|
/* The reference reduces the nonce hash modulo the group order. */
|
|
|
|
|
secp256k1_scalar_set_b32(&k, nonce32, NULL);
|
|
|
|
|
ret &= !secp256k1_scalar_is_zero(&k);
|
|
|
|
|
secp256k1_scalar_cmov(&k, &secp256k1_scalar_one, !ret);
|
|
|
|
|
|
|
|
|
|
secp256k1_ecmult_gen_ge(&ctx->ecmult_gen_ctx, &r, &k);
|
|
|
|
|
/* We declassify r to allow using it as a branch point. This is fine
|
|
|
|
|
* because r is not a secret. */
|
|
|
|
|
secp256k1_declassify(ctx, &r, sizeof(r));
|
|
|
|
|
secp256k1_fe_normalize_var(&r.y);
|
|
|
|
|
if (secp256k1_fe_is_odd(&r.y)) {
|
|
|
|
|
secp256k1_scalar_negate(&k, &k);
|
|
|
|
|
}
|
|
|
|
|
secp256k1_fe_normalize_var(&r.x);
|
|
|
|
|
secp256k1_fe_get_b32(&sig64[0], &r.x);
|
|
|
|
|
|
|
|
|
|
secp256k1_chilldkg_schnorrsig_challenge(hash_ctx, &e, &sig64[0], msg, msglen, pk32, tag_prefix);
|
|
|
|
|
secp256k1_scalar_mul(&e, &e, &sk);
|
|
|
|
|
secp256k1_scalar_add(&e, &e, &k);
|
|
|
|
|
secp256k1_scalar_get_b32(&sig64[32], &e);
|
|
|
|
|
|
|
|
|
|
secp256k1_memczero(sig64, 64, !ret);
|
|
|
|
|
secp256k1_scalar_clear(&k);
|
|
|
|
|
secp256k1_scalar_clear(&sk);
|
|
|
|
|
secp256k1_scalar_clear(&e);
|
|
|
|
|
secp256k1_ge_clear(&pk);
|
|
|
|
|
secp256k1_ge_clear(&r);
|
|
|
|
|
secp256k1_memclear_explicit(seckey, sizeof(seckey));
|
|
|
|
|
secp256k1_memclear_explicit(nonce32, sizeof(nonce32));
|
|
|
|
|
|
chilldkg: CI wiring, ctime_tests coverage, declassify fixes
CI:
- ci/ci.sh: new CHILLDKG environment variable, passed to configure as
--enable-module-chilldkg (mirroring FROST).
- .github/workflows/ci.yml: default CHILLDKG: 'no' and CHILLDKG: 'yes'
in every job that enables FROST, except the x86_64 matrix entry that
deliberately builds without the ecdh module (chilldkg requires
schnorrsig + ecdh; the configure-time dependency error would fire
there). YAML validity and per-job dependency presence checked
programmatically.
ctime_tests:
- src/ctime_tests.c: run a full ChillDKG session (n = 2, t = 2) through
the public API under the memory checker: hostpubkey_gen, params_hash,
participant_step1, coordinator_step1, participant_step2,
coordinator_finalize, participant_finalize, participant_recover and
recovery_ack_sign. Host secret keys, session randomness, aux
randomness and the resulting secret shares are undefined (secret);
all protocol messages, the certificate, threshold public key, public
shares, recovery data, ack signature and the secret-free state1
objects are defined (public). state2 stays secret (contains the
secret share).
Constant-time fixes found by running the new block under
MemorySanitizer (valgrind unavailable locally; MSan build via clang +
CMake). All are missing declassifications of secret-derived but public
(or public-outcome) values, following the frost module's
secp256k1_declassify pattern with justification comments; no real
constant-time bugs were found:
- hostpubkey_gen: declassify the computed host public key before
serialization (public output).
- participant_step1: declassify the zero-randomness check result (only
reveals "the RNG returned 32 zero bytes", which aborts the session).
- encpedpop participant_step1: declassify the pubnonce point before
serialization (public, part of pmsg1).
- chilldkg_schnorrsig_sign: declassify the signer public key before
normalization/parity branch, and declassify the return value (a
failure only reveals a zero derived nonce, negligible probability).
- vss_commit: declassify the VSS commitments before serialization
(public, part of pmsg1).
- vss_verify_secshare: declassify secshare*G before the infinity/eq
checks (equals the public pubshare in honest runs; the discrete log
is not revealed).
- simplpedpop_participant_investigate (proactive audit; not reached by
ctime_tests): declassify the secshare-sum comparison result (the
public fault code reveals it anyway).
Verified: MSan ctime_tests exits 0; autotools make check 10/10 (the
local tree is configured without --enable-ctime-tests because neither
valgrind nor an MSan-instrumented gcc build is available; CI runs
ctime_tests under valgrind as before); CMake ctest 428/428;
./tests --target=chilldkg and ./chilldkg_example pass.
2026-08-31 10:25:14 +02:00
|
|
|
/* Branching on the return value only leaks whether the derived nonce is
|
|
|
|
|
* zero, which happens with negligible probability (the case of an invalid
|
|
|
|
|
* secret key is declassified above). */
|
|
|
|
|
secp256k1_declassify(ctx, &ret, sizeof(ret));
|
chilldkg: Phase 1 - internal primitives (util, vss)
Add the byte-exact internal primitives for the ChillDKG module,
mirroring the Python reference implementation of the bip-frost-dkg
draft (v0.3.0-dev), pinned to upstream commit
a91896883f85b159415ecf298d5e844879af112d.
util.h / util_impl.h (mirrors chilldkg_ref/util.py):
- Point (de)serialization with explicit point-at-infinity support:
33 zero bytes <-> infinity, otherwise SEC compressed. Checked parse
rejects invalid encodings and out-of-range x coordinates
(point_save/point_load, xonly_save/xonly_load).
- Internal parameterized-tag BIP-340 Schnorr sign/verify
(chilldkg_schnorrsig_sign/_verify): tag prefix selects the
<prefix>/aux, /nonce, /challenge subtags ("BIP DKG/pop message" for
proofs of possession, "BIP0340" for CertEq signatures and recovery
acks), arbitrary-length messages, pad33 zero-padding helper. The
public schnorrsig API hardcodes BIP0340/32-byte messages, so the
algorithm is replicated from secp256k1_schnorrsig_sign_internal with
a custom tag; cross-checked against secp256k1_schnorrsig_sign32.
- Tagged hashes via secp256k1_sha256_initialize_tagged:
"BIP DKG/params_hash", "BIP DKG/encpedpop seed",
"BIP DKG/simplpedpop aux", "BIP DKG/encpedpop secnonce",
"BIP DKG/encpedpop ecdh", "BIP DKG/encaps_multi self_pad",
"BIP DKG/vss coeffs", and BIP-341 "TapTweak" (32-byte x-only input).
- params_hash = TH("BIP DKG/params_hash", u32be(t) || hostpubkeys)
(note: plan had the operand order reversed; the reference hashes t
first).
- ECDH pads: reuses the ecdh module's SHA256-of-compressed-shared-
point hash, then TH("BIP DKG/encpedpop ecdh", ecdh ||
sender_pubnonce || receiver_hostpubkey || context) with a sending
flag fixing the sender|receiver order; self_pad for the own index.
Pads are parsed wrapping (mod-n reduction); wire scalars, VSS
coefficients and the TapTweak are parsed checked.
vss.h / vss_impl.h (mirrors chilldkg_ref/vss.py):
- vss_gen_coeffs: per-coefficient TH("BIP DKG/vss coeffs", seed ||
u32be(j)), checked parse with bitwise error accumulation.
- vss_poly_eval (Horner) and vss_secshare_for with the x = id+1
convention (safe at UINT32_MAX).
- vss_commit (constant-time ecmult_gen, zero coefficient -> infinity),
vss_pubshare (powers-of-x over commitments, skips infinity),
vss_commitment_add, vss_verify_secshare.
- vss_invalid_taproot_commit: TapTweak applied to the x-only constant
term so the Taproot script path is unspendable; returns tweak and
pubtweak.
tests_impl.h: 7 vector tests (tagged hashes, params_hash, point
serialization incl. infinity roundtrip and parity prefixes, checked
vs wrapping scalar parse at the group order boundary, custom-tag
schnorrsig incl. wrong-tag/key/msg rejection, ECDH pad sender/receiver
symmetry, VSS coeff derivation/Horner/commitment/pubshare/tweak) with
expected values generated once from the Python reference
(committed into the test file, reference commit recorded).
Verified: make check 3/3 suites pass; CMake ctest all pass;
./tests --target=chilldkg runs all 7 new tests green in both verify
and noverify builds.
2026-08-31 04:05:15 +02:00
|
|
|
return ret;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Mirrors schnorr_verify in secp256k1lab/bip340.py. The structure follows
|
|
|
|
|
* secp256k1_schnorrsig_verify, which cannot be reused directly because it
|
|
|
|
|
* hardcodes the "BIP0340/challenge" tag. */
|
|
|
|
|
static int secp256k1_chilldkg_schnorrsig_verify(const secp256k1_context *ctx, const unsigned char *sig64, const unsigned char *msg, size_t msglen, const unsigned char *pubkey32, const char *tag_prefix) {
|
|
|
|
|
secp256k1_scalar s;
|
|
|
|
|
secp256k1_scalar e;
|
|
|
|
|
secp256k1_gej rj;
|
|
|
|
|
secp256k1_ge pk;
|
|
|
|
|
secp256k1_gej pkj;
|
|
|
|
|
secp256k1_fe rx;
|
|
|
|
|
secp256k1_ge r;
|
|
|
|
|
unsigned char buf[32];
|
|
|
|
|
int overflow;
|
|
|
|
|
|
|
|
|
|
VERIFY_CHECK(ctx != NULL);
|
|
|
|
|
ARG_CHECK(sig64 != NULL);
|
|
|
|
|
ARG_CHECK(msg != NULL || msglen == 0);
|
|
|
|
|
ARG_CHECK(pubkey32 != NULL);
|
|
|
|
|
ARG_CHECK(tag_prefix != NULL);
|
|
|
|
|
|
|
|
|
|
if (!secp256k1_fe_set_b32_limit(&rx, &sig64[0])) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
secp256k1_scalar_set_b32(&s, &sig64[32], &overflow);
|
|
|
|
|
if (overflow) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!secp256k1_chilldkg_xonly_load(&pk, pubkey32)) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Compute e. */
|
|
|
|
|
secp256k1_fe_get_b32(buf, &pk.x);
|
|
|
|
|
secp256k1_chilldkg_schnorrsig_challenge(secp256k1_get_hash_context(ctx), &e, &sig64[0], msg, msglen, buf, tag_prefix);
|
|
|
|
|
|
|
|
|
|
/* Compute rj = s*G + (-e)*pkj */
|
|
|
|
|
secp256k1_scalar_negate(&e, &e);
|
|
|
|
|
secp256k1_gej_set_ge(&pkj, &pk);
|
|
|
|
|
secp256k1_ecmult(&rj, &pkj, &e, &s);
|
|
|
|
|
|
|
|
|
|
secp256k1_ge_set_gej_var(&r, &rj);
|
|
|
|
|
if (secp256k1_ge_is_infinity(&r)) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
secp256k1_fe_normalize_var(&r.y);
|
|
|
|
|
return !secp256k1_fe_is_odd(&r.y) &&
|
|
|
|
|
secp256k1_fe_equal(&rx, &r.x);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void secp256k1_chilldkg_encpedpop_ecdh(const secp256k1_context *ctx, secp256k1_scalar *out, const secp256k1_scalar *seckey, const secp256k1_ge *their_point, const unsigned char *my_pubkey33, const unsigned char *their_pubkey33, const unsigned char *context, size_t context_len, int sending) {
|
|
|
|
|
const secp256k1_hash_ctx *hash_ctx = secp256k1_get_hash_context(ctx);
|
|
|
|
|
secp256k1_gej resj;
|
|
|
|
|
secp256k1_ge res;
|
|
|
|
|
unsigned char x[32];
|
|
|
|
|
unsigned char y[32];
|
|
|
|
|
unsigned char shared[32];
|
|
|
|
|
unsigned char hash32[32];
|
|
|
|
|
secp256k1_sha256 sha;
|
|
|
|
|
|
|
|
|
|
VERIFY_CHECK(ctx != NULL);
|
|
|
|
|
VERIFY_CHECK(!secp256k1_scalar_is_zero(seckey));
|
|
|
|
|
VERIFY_CHECK(!secp256k1_ge_is_infinity(their_point));
|
|
|
|
|
|
|
|
|
|
/* libsecp256k1-style ECDH: SHA256 of the compressed shared point. */
|
|
|
|
|
secp256k1_ecmult_const(&resj, their_point, seckey);
|
|
|
|
|
secp256k1_ge_set_gej(&res, &resj);
|
|
|
|
|
/* The result cannot be the point at infinity: their_point is not
|
|
|
|
|
* infinity, seckey is nonzero, and the group has prime order. */
|
|
|
|
|
VERIFY_CHECK(!secp256k1_ge_is_infinity(&res));
|
|
|
|
|
secp256k1_fe_normalize(&res.x);
|
|
|
|
|
secp256k1_fe_normalize(&res.y);
|
|
|
|
|
secp256k1_fe_get_b32(x, &res.x);
|
|
|
|
|
secp256k1_fe_get_b32(y, &res.y);
|
|
|
|
|
/* This hash function always succeeds; call it unconditionally (it must
|
|
|
|
|
* not sit inside VERIFY_CHECK, which is compiled out in noverify
|
|
|
|
|
* builds). */
|
|
|
|
|
if (!ecdh_hash_function_sha256_impl(hash_ctx, shared, x, y, NULL)) {
|
|
|
|
|
VERIFY_CHECK(0);
|
|
|
|
|
memset(shared, 0, sizeof(shared));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
secp256k1_chilldkg_sha256_tagged_encpedpop_ecdh(hash_ctx, &sha);
|
|
|
|
|
secp256k1_sha256_write(hash_ctx, &sha, shared, 32);
|
|
|
|
|
/* The sender's pubnonce always comes first in the hash input. */
|
|
|
|
|
if (sending) {
|
|
|
|
|
secp256k1_sha256_write(hash_ctx, &sha, my_pubkey33, 33);
|
|
|
|
|
secp256k1_sha256_write(hash_ctx, &sha, their_pubkey33, 33);
|
|
|
|
|
} else {
|
|
|
|
|
secp256k1_sha256_write(hash_ctx, &sha, their_pubkey33, 33);
|
|
|
|
|
secp256k1_sha256_write(hash_ctx, &sha, my_pubkey33, 33);
|
|
|
|
|
}
|
|
|
|
|
secp256k1_sha256_write(hash_ctx, &sha, context, context_len);
|
|
|
|
|
secp256k1_sha256_finalize(hash_ctx, &sha, hash32);
|
|
|
|
|
secp256k1_sha256_clear(&sha);
|
|
|
|
|
|
|
|
|
|
/* Pads are reduced modulo the group order (from_bytes_wrapping in the
|
|
|
|
|
* reference). */
|
|
|
|
|
secp256k1_scalar_set_b32(out, hash32, NULL);
|
|
|
|
|
|
|
|
|
|
secp256k1_memclear_explicit(x, sizeof(x));
|
|
|
|
|
secp256k1_memclear_explicit(y, sizeof(y));
|
|
|
|
|
secp256k1_memclear_explicit(shared, sizeof(shared));
|
|
|
|
|
secp256k1_memclear_explicit(hash32, sizeof(hash32));
|
|
|
|
|
secp256k1_ge_clear(&res);
|
|
|
|
|
secp256k1_gej_clear(&resj);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void secp256k1_chilldkg_encpedpop_self_pad(const secp256k1_hash_ctx *hash_ctx, secp256k1_scalar *out, const unsigned char *symkey32, const unsigned char *nonce33, const unsigned char *context, size_t context_len) {
|
|
|
|
|
unsigned char hash32[32];
|
|
|
|
|
secp256k1_sha256 sha;
|
|
|
|
|
|
|
|
|
|
secp256k1_chilldkg_sha256_tagged_self_pad(hash_ctx, &sha);
|
|
|
|
|
secp256k1_sha256_write(hash_ctx, &sha, symkey32, 32);
|
|
|
|
|
secp256k1_sha256_write(hash_ctx, &sha, nonce33, 33);
|
|
|
|
|
secp256k1_sha256_write(hash_ctx, &sha, context, context_len);
|
|
|
|
|
secp256k1_sha256_finalize(hash_ctx, &sha, hash32);
|
|
|
|
|
secp256k1_sha256_clear(&sha);
|
|
|
|
|
|
|
|
|
|
/* from_bytes_wrapping in the reference. */
|
|
|
|
|
secp256k1_scalar_set_b32(out, hash32, NULL);
|
|
|
|
|
secp256k1_memclear_explicit(hash32, sizeof(hash32));
|
|
|
|
|
}
|
|
|
|
|
|
chilldkg: Phase 3 - public participant API and CertEq
Add the public participant-facing ChillDKG API to
include/secp256k1_chilldkg.h and the CertEq sub-protocol, completing
the participant side of the protocol (bip-frost-dkg v0.3.0-dev,
reference pinned at a91896883f85b159415ecf298d5e844879af112d).
New module files:
- certeq.h / certeq_impl.h: CertEq sub-protocol. Participants sign
pad33("BIP DKG/certeq message") || u32be(i) || eq_input with plain
BIP0340-tagged Schnorr signatures under their host key
(certeq_participant_step); verification is per-index against the
x-only hostpubkeys[i][1:33] exactly as the reference
(certeq_verify). The coordinator side reuses certeq_verify in
Phase 4.
Public API (all no-malloc, caller-allocated buffers, outputs zeroed on
failure, secret paths cleared):
- secp256k1_chilldkg_hostpubkey_gen: plain compressed host pubkey
generation; rejects zero / >= group order seckeys.
- secp256k1_chilldkg_params_hash: validates session params (participant
and threshold ranges, strictly compressed non-infinity pubkeys, no
duplicates) and computes TH("BIP DKG/params_hash", u32be(t) ||
hostpubkeys).
- Message-length helpers so callers can size buffers:
participant_msg1_len (33t+32n+97), coordinator_msg1_len
(162n+33(t-1)), participant_msg2_len (64), coordinator_msg2_len
(64n), recovery_data_len (4+33t+162n).
- secp256k1_chilldkg_participant_step1: full EncPedPop step1 with
seed=deckey=hostseckey; rejects zero randomness and hostseckeys not
matching the claimed hostpubkey (input errors, not protocol faults).
- secp256k1_chilldkg_participant_step2: parses and verifies cmsg1 via
the Phase 2 encpedpop/simplpedpop participant path, computes the
tweaked secshare/pubshares/threshold pubkey, appends enc_secshares
to eq_input (matching the reference for recovery consistency), and
emits the 64-byte CertEq signature.
- secp256k1_chilldkg_participant_finalize: re-verifies all n CertEq
signatures in the certificate, then outputs the 32-byte secshare,
33-byte threshold pubkey, n pubshares and the self-delimiting
recovery data (eq_input || cert).
Blame reporting without exceptions: public enum
secp256k1_chilldkg_fault (OK / FAULTY_COORDINATOR /
FAULTY_PARTICIPANT / FAULTY_PARTICIPANT_OR_COORDINATOR /
UNKNOWN_FAULTY_PARTICIPANT_OR_COORDINATOR / INVALID_INPUT) plus an out
fault_index, mapping the reference's exception taxonomy:
- hostseckey invalid/mismatch -> INVALID_INPUT (HostSeckeyError),
- cmsg1 scalar overflow/parse -> FAULTY_COORDINATOR (MsgParseError),
- pubnonce/commitment/PoP faults -> FAULTY_PARTICIPANT_OR_COORDINATOR(i),
- share-vs-pubshare mismatch -> UNKNOWN with fault_index = UINT32_MAX,
- certificate signature failure -> FAULTY_COORDINATOR (documented
deviation: fault_index carries the failing signature index as
diagnostic info; the reference discards it).
Enum-returning functions use a local CHILLDKG_ARG_CHECK that fires the
illegal-argument callback and returns INVALID_INPUT (ARG_CHECK would
return 0 = OK).
Opaque state objects with magic-validated save/load (frost idiom):
participant_state1 (4306 bytes, no secrets) and participant_state2
(21073 bytes, contains the secshare; documented keep-secret/no-copy).
Fixed-size at SECP256K1_CHILLDKG_MAX_PARTICIPANTS = 128.
Also fixes a noverify-build bug: state1_load ran point_load inside
VERIFY_CHECK, which compiles out in noverify builds and left the
commitment uninitialized; now called unconditionally.
tests_impl.h: participant_api_test with full-session reference vectors
(n=3, t=2; coordinator aggregation simulated through the internal
Phase 2 coordinator step and verified byte-identical to the
reference's coordinator_step1): msglen helpers, hostpubkey_gen and
params_hash vectors incl. duplicate/invalid/infinity rejection,
byte-exact pmsg1/cmsg1/CertEq sigs/secshare/thresh_pk/pubshares/
recovery, blame cases (tampered enc_secshare -> UNKNOWN, invalid
pubnonce -> FAULTY_PARTICIPANT_OR_COORDINATOR(1), overflowing
enc_secshare -> FAULTY_COORDINATOR, corrupted cert sig ->
FAULTY_COORDINATOR with fault_index and zeroed outputs), NULL-arg
misuse and bad-magic state rejection.
Verified: make check 3/3 (incl. noverify); CMake ctest 363/363;
make distdir includes all new files.
2026-08-31 05:22:53 +02:00
|
|
|
static void secp256k1_chilldkg_params_hash_internal(const secp256k1_hash_ctx *hash_ctx, unsigned char *out32, const unsigned char *hostpubkeys33, size_t n, uint32_t t) {
|
chilldkg: Phase 1 - internal primitives (util, vss)
Add the byte-exact internal primitives for the ChillDKG module,
mirroring the Python reference implementation of the bip-frost-dkg
draft (v0.3.0-dev), pinned to upstream commit
a91896883f85b159415ecf298d5e844879af112d.
util.h / util_impl.h (mirrors chilldkg_ref/util.py):
- Point (de)serialization with explicit point-at-infinity support:
33 zero bytes <-> infinity, otherwise SEC compressed. Checked parse
rejects invalid encodings and out-of-range x coordinates
(point_save/point_load, xonly_save/xonly_load).
- Internal parameterized-tag BIP-340 Schnorr sign/verify
(chilldkg_schnorrsig_sign/_verify): tag prefix selects the
<prefix>/aux, /nonce, /challenge subtags ("BIP DKG/pop message" for
proofs of possession, "BIP0340" for CertEq signatures and recovery
acks), arbitrary-length messages, pad33 zero-padding helper. The
public schnorrsig API hardcodes BIP0340/32-byte messages, so the
algorithm is replicated from secp256k1_schnorrsig_sign_internal with
a custom tag; cross-checked against secp256k1_schnorrsig_sign32.
- Tagged hashes via secp256k1_sha256_initialize_tagged:
"BIP DKG/params_hash", "BIP DKG/encpedpop seed",
"BIP DKG/simplpedpop aux", "BIP DKG/encpedpop secnonce",
"BIP DKG/encpedpop ecdh", "BIP DKG/encaps_multi self_pad",
"BIP DKG/vss coeffs", and BIP-341 "TapTweak" (32-byte x-only input).
- params_hash = TH("BIP DKG/params_hash", u32be(t) || hostpubkeys)
(note: plan had the operand order reversed; the reference hashes t
first).
- ECDH pads: reuses the ecdh module's SHA256-of-compressed-shared-
point hash, then TH("BIP DKG/encpedpop ecdh", ecdh ||
sender_pubnonce || receiver_hostpubkey || context) with a sending
flag fixing the sender|receiver order; self_pad for the own index.
Pads are parsed wrapping (mod-n reduction); wire scalars, VSS
coefficients and the TapTweak are parsed checked.
vss.h / vss_impl.h (mirrors chilldkg_ref/vss.py):
- vss_gen_coeffs: per-coefficient TH("BIP DKG/vss coeffs", seed ||
u32be(j)), checked parse with bitwise error accumulation.
- vss_poly_eval (Horner) and vss_secshare_for with the x = id+1
convention (safe at UINT32_MAX).
- vss_commit (constant-time ecmult_gen, zero coefficient -> infinity),
vss_pubshare (powers-of-x over commitments, skips infinity),
vss_commitment_add, vss_verify_secshare.
- vss_invalid_taproot_commit: TapTweak applied to the x-only constant
term so the Taproot script path is unspendable; returns tweak and
pubtweak.
tests_impl.h: 7 vector tests (tagged hashes, params_hash, point
serialization incl. infinity roundtrip and parity prefixes, checked
vs wrapping scalar parse at the group order boundary, custom-tag
schnorrsig incl. wrong-tag/key/msg rejection, ECDH pad sender/receiver
symmetry, VSS coeff derivation/Horner/commitment/pubshare/tweak) with
expected values generated once from the Python reference
(committed into the test file, reference commit recorded).
Verified: make check 3/3 suites pass; CMake ctest all pass;
./tests --target=chilldkg runs all 7 new tests green in both verify
and noverify builds.
2026-08-31 04:05:15 +02:00
|
|
|
unsigned char buf[4];
|
|
|
|
|
secp256k1_sha256 sha;
|
|
|
|
|
|
|
|
|
|
secp256k1_chilldkg_sha256_tagged_params_hash(hash_ctx, &sha);
|
|
|
|
|
secp256k1_write_be32(buf, t);
|
|
|
|
|
secp256k1_sha256_write(hash_ctx, &sha, buf, sizeof(buf));
|
|
|
|
|
secp256k1_sha256_write(hash_ctx, &sha, hostpubkeys33, 33 * n);
|
|
|
|
|
secp256k1_sha256_finalize(hash_ctx, &sha, out32);
|
|
|
|
|
secp256k1_sha256_clear(&sha);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endif
|