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_H
|
|
|
|
|
#define SECP256K1_MODULE_CHILLDKG_UTIL_H
|
|
|
|
|
|
|
|
|
|
#include "../../../include/secp256k1.h"
|
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
|
|
|
#include "../../../include/secp256k1_chilldkg.h"
|
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
|
|
|
|
|
|
|
|
#include "../../group.h"
|
|
|
|
|
#include "../../hash.h"
|
|
|
|
|
#include "../../scalar.h"
|
|
|
|
|
|
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
|
|
|
/* The public header defines SECP256K1_CHILLDKG_MAX_PARTICIPANTS and the fault
|
|
|
|
|
* enum secp256k1_chilldkg_fault (SECP256K1_CHILLDKG_OK, ...), which the
|
|
|
|
|
* internal SimplPedPop/EncPedPop functions use directly as their return
|
|
|
|
|
* codes. */
|
chilldkg: Phase 2 - SimplPedPop and EncPedPop layers
Port the two sub-protocol layers of the ChillDKG reference
implementation (bip-frost-dkg v0.3.0-dev, upstream commit
a91896883f85b159415ecf298d5e844879af112d) to C, keeping the same
function decomposition as simplpedpop.py / encpedpop.py so the code
stays diffable against the reference.
simplpedpop.h / simplpedpop_impl.h (mirrors simplpedpop.py):
- simplpedpop_participant_step1: VSS coefficient generation and
commitment, per-participant shares, proof of possession. The PoP is
a BIP-340 signature with custom tag prefix "BIP DKG/pop message"
over u32be(participant_id), signed with the constant coefficient
f(0), using TH("BIP DKG/simplpedpop aux", simpl_seed) as aux_rand.
- simplpedpop_coordinator_step / assemble_sum_coms: echo per-dealer
constant-term commitments, sum non-constant-term commitments,
collect PoPs (not verified by the coordinator, as in the reference).
- simplpedpop_participant_step2: own-commitment echo check, per-dealer
infinity rejection and PoP verification against the x-only
coms_to_secrets[i], TapTweak applied before share verification
(secshare vs pubshare check on tweaked values), eq_input =
u32be(t) || sum_coms committing to the UNTWEAKED summed commitment.
encpedpop.h / encpedpop_impl.h (mirrors encpedpop.py):
- simpl_seed / aux / secnonce derivations from (hostseckey, random,
enc_context) via "BIP DKG/encpedpop seed", "BIP DKG/simplpedpop aux"
and "BIP DKG/encpedpop secnonce"; pubnonce = pubkey_gen_plain(secnonce).
- encaps_multi / encrypt_multi: per-recipient pad context
u32be(i) || enc_context, self-pad at the own index (no ECDH),
libsecp256k1-style ECDH pad otherwise with sender-first ordering;
encryption is additive mod n so the coordinator can sum encrypted
shares per recipient.
- decaps_multi / decrypt_sum: receiver-side pads, invalid or infinity
pubnonce maps to FAULTY_PARTICIPANT_OR_COORDINATOR(sender).
- coordinator_step: checked scalar parse of encrypted shares
(overflow blames the sender), per-recipient summation.
- participant_step2: pubnonce echo check (mismatch ->
FAULTY_COORDINATOR), decrypt, delegate to simplpedpop step2,
eq_input extended with enckeys || pubnonces.
util.h: add SECP256K1_CHILLDKG_MAX_PARTICIPANTS (128, matching frost)
and the internal fault enum mirroring the reference's exception
taxonomy (FAULTY_COORDINATOR, FAULTY_PARTICIPANT,
FAULTY_PARTICIPANT_OR_COORDINATOR,
UNKNOWN_FAULTY_PARTICIPANT_OR_COORDINATOR, INVALID_INPUT). The public
blame-reporting enum arrives with the Phase 3 API.
State structs are fixed-size (no malloc, cap 128 participants). All
secret temporaries are cleared; negligible-probability secret-
dependent failures are declassified before branching.
Documented deviations where the reference crashes with non-protocol
errors: infinity sum_coms[0] in invalid_taproot_commit (unreachable
after PoP verification) and tweak-hash overflow (negligible) return
UNKNOWN_FAULTY_PARTICIPANT_OR_COORDINATOR; a wrong cmsg length maps
to FAULTY_COORDINATOR. Investigation procedures are deferred to
Phase 5 per the plan.
tests_impl.h: byte-exact n=3/t=2 happy-path vectors for both layers
generated from the Python reference (pmsg/cmsg/eq_input/enc_secshares
and all DKG outputs), plus PoP reject cases (tampered, wrong index,
infinity commitment, wrong echo, tampered non-constant-term sum),
coordinator blame (bad commitment, overflowing encrypted share),
encaps/decaps pad symmetry for all sender/receiver pairs including
self-pad, encrypt->sum->decrypt_sum roundtrip, tampered encrypted
share, invalid pubnonce blame, and input-validation rejects.
Verified: make check 3/3 suites pass (incl. noverify_tests running the
module); CMake ctest 361/361; ./tests --target=chilldkg runs all 9
module tests green.
2026-08-31 04:47:38 +02:00
|
|
|
|
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
|
|
|
/* This file contains the internal primitives of the ChillDKG module that
|
|
|
|
|
* mirror chilldkg_ref/util.py, secp256k1lab/bip340.py and secp256k1lab/ecdh.py
|
|
|
|
|
* of the bip-frost-dkg reference implementation. Byte-exactness with the
|
|
|
|
|
* reference is the goal; the tagged hash strings below are copied verbatim
|
|
|
|
|
* from the Python sources. */
|
|
|
|
|
|
|
|
|
|
/* Tagged hash initializers (BIP 340 tagged hashes). All ChillDKG tags share
|
|
|
|
|
* the "BIP DKG/" prefix (BIP_TAG in chilldkg_ref/util.py); "TapTweak" is the
|
|
|
|
|
* BIP 341 tag. */
|
|
|
|
|
static void secp256k1_chilldkg_sha256_tagged_params_hash(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha);
|
|
|
|
|
static void secp256k1_chilldkg_sha256_tagged_encpedpop_seed(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha);
|
|
|
|
|
static void secp256k1_chilldkg_sha256_tagged_simplpedpop_aux(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha);
|
|
|
|
|
static void secp256k1_chilldkg_sha256_tagged_encpedpop_secnonce(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha);
|
|
|
|
|
static void secp256k1_chilldkg_sha256_tagged_encpedpop_ecdh(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha);
|
|
|
|
|
static void secp256k1_chilldkg_sha256_tagged_self_pad(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha);
|
|
|
|
|
static void secp256k1_chilldkg_sha256_tagged_vss_coeffs(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha);
|
|
|
|
|
static void secp256k1_chilldkg_sha256_tagged_taptweak(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha);
|
|
|
|
|
|
|
|
|
|
/* Serialize a group element to 33 bytes: the point at infinity maps to 33
|
|
|
|
|
* zero bytes, otherwise the standard SEC compressed encoding (0x02/0x03
|
|
|
|
|
* prefix) is used. Mirrors GE.to_bytes_compressed_with_infinity. */
|
|
|
|
|
static void secp256k1_chilldkg_point_save(unsigned char *out33, const secp256k1_ge *p);
|
|
|
|
|
|
|
|
|
|
/* Parse a 33-byte group element encoding: 33 zero bytes map to the point at
|
|
|
|
|
* infinity, otherwise standard SEC compressed parsing applies. Returns 1 on
|
|
|
|
|
* success and 0 if the encoding is invalid (wrong prefix, x >= p, or x not on
|
|
|
|
|
* the curve). On failure, *p is set to the point at infinity. Mirrors
|
|
|
|
|
* GE.from_bytes_compressed_with_infinity. */
|
|
|
|
|
static int secp256k1_chilldkg_point_load(secp256k1_ge *p, const unsigned char *in33);
|
|
|
|
|
|
|
|
|
|
/* Serialize the x coordinate of a non-infinity group element (32 bytes).
|
|
|
|
|
* Mirrors GE.to_bytes_xonly, which asserts non-infinity. */
|
|
|
|
|
static void secp256k1_chilldkg_xonly_save(unsigned char *out32, const secp256k1_ge *p);
|
|
|
|
|
|
|
|
|
|
/* Parse a 32-byte x-only group element encoding (even y). Returns 1 on
|
|
|
|
|
* success, 0 if x >= p or x is not on the curve. Mirrors
|
|
|
|
|
* GE.from_bytes_xonly. */
|
|
|
|
|
static int secp256k1_chilldkg_xonly_load(secp256k1_ge *p, const unsigned char *in32);
|
|
|
|
|
|
|
|
|
|
/* Write str (at most 33 bytes) zero-padded to 33 bytes. Used to build the
|
|
|
|
|
* domain separation prefixes of the CertEq and recovery acknowledgment
|
|
|
|
|
* messages (see certeq_message/recovery_ack_message in chilldkg_ref). */
|
|
|
|
|
static void secp256k1_chilldkg_pad33(unsigned char *out33, const char *str);
|
|
|
|
|
|
|
|
|
|
/* BIP 340 Schnorr signing/verification with a parameterized tag prefix and
|
|
|
|
|
* arbitrary-length messages, mirroring secp256k1lab/bip340.py's schnorr_sign
|
|
|
|
|
* and schnorr_verify. The tags used for aux, nonce and challenge hashes are
|
|
|
|
|
* tag_prefix || "/aux", tag_prefix || "/nonce" and tag_prefix ||
|
|
|
|
|
* "/challenge". ChillDKG uses tag_prefix "BIP DKG/pop message" for proofs of
|
|
|
|
|
* possession (POP_MSG_TAG in simplpedpop.py) and the standard "BIP0340"
|
|
|
|
|
* prefix for CertEq and recovery acknowledgments.
|
|
|
|
|
*
|
|
|
|
|
* sign returns 1 on success and 0 if seckey32 is not in range 1..n-1 or the
|
|
|
|
|
* derived nonce is zero (negligible probability). On failure, sig64 is set to
|
|
|
|
|
* zero. verify returns 1 if the signature is valid and 0 otherwise. */
|
|
|
|
|
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);
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
/* Compute the EncPedPop ECDH pad (encpedpop.py `ecdh`):
|
|
|
|
|
* shared = SHA256(compressed(seckey * their_point)) [libsecp256k1 ECDH]
|
|
|
|
|
* out = Scalar.from_bytes_wrapping(TH("BIP DKG/encpedpop ecdh",
|
|
|
|
|
* shared || my_pubkey33 || their_pubkey33 || context)) if sending
|
|
|
|
|
* out = Scalar.from_bytes_wrapping(TH("BIP DKG/encpedpop ecdh",
|
|
|
|
|
* shared || their_pubkey33 || my_pubkey33 || context)) otherwise
|
|
|
|
|
* In the protocol, my/their_pubkey33 are the sender's pubnonce and the
|
|
|
|
|
* receiver's host pubkey (in this order in the hash input, regardless of
|
|
|
|
|
* sending), and context is u32be(receiver_index) || enc_context.
|
|
|
|
|
* their_point must be the parsed their_pubkey33 and must not be infinity
|
|
|
|
|
* (enforced by VERIFY_CHECK). seckey must be nonzero. */
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
/* Compute the EncPedPop symmetric pad for encrypting to ourselves
|
|
|
|
|
* (encpedpop.py `self_pad`):
|
|
|
|
|
* out = Scalar.from_bytes_wrapping(TH("BIP DKG/encaps_multi self_pad",
|
|
|
|
|
* symkey32 || nonce33 || context))
|
|
|
|
|
* where symkey32 is the host seckey, nonce33 is the own pubnonce and context
|
|
|
|
|
* is u32be(own_index) || enc_context. */
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
/* Compute the session parameters hash (chilldkg.py `params_hash`):
|
|
|
|
|
* out32 = TH("BIP DKG/params_hash", u32be(t) || hostpubkeys[0] || ... || hostpubkeys[n-1])
|
|
|
|
|
* where hostpubkeys33 is an array of n 33-byte compressed host public keys. */
|
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
|
|
|
|
|
|
|
|
#endif
|