chilldkg: Phase 0 - module scaffolding and build wiring
Add an empty, experimental `chilldkg` module as the foundation for a
ChillDKG implementation (distributed key generation for FROST) per the
bip-frost-dkg BIP draft (v0.3.0-dev):
https://github.com/BlockstreamResearch/bip-frost-dkg
The module lives in src/modules/chilldkg/ (separate from the frost
module, per the implementation plan in .idea/docs/
chilldkg-implementation-plan.md: FROST signing (BIP 445) and ChillDKG
are separate BIPs with separate reference repos, test vectors and
review cycles; the dependency between them is one-way bytes).
New files:
- include/secp256k1_chilldkg.h: public header skeleton with the same
"EXTREMELY DANGEROUS / work in progress" warning style as
secp256k1_frost.h, plus a note that the BIP is a draft and tagged
hashes/wire formats may change. No API yet (Phase 3+).
- src/modules/chilldkg/main_impl.h: implementation skeleton including
the public header.
- src/modules/chilldkg/tests_impl.h: trivial scaffolding unit test
(chilldkg_scaffolding_test) registered via the tests_chilldkg[]
CASE1 array used by this repo's unit-test framework.
- src/modules/chilldkg/Makefile.am.include: autotools file list,
mirroring the frost module's.
- src/modules/chilldkg/chilldkg.md: module doc stub (purpose, draft
status, dependency on the schnorrsig and ecdh modules).
Build wiring (mirrors the frost module exactly):
- configure.ac: --enable-module-chilldkg (default no, experimental
gate), dependency errors when schnorrsig or ecdh are explicitly
disabled, AM_CONDITIONAL(ENABLE_MODULE_CHILLDKG), summary line.
- Makefile.am: include src/modules/chilldkg/Makefile.am.include under
ENABLE_MODULE_CHILLDKG.
- src/secp256k1.c: guarded include of modules/chilldkg/main_impl.h
after the frost module.
- src/tests.c: guarded include of tests_impl.h and
MAKE_TEST_MODULE(chilldkg) registration.
- CMakeLists.txt: SECP256K1_ENABLE_MODULE_CHILLDKG option (OFF) +
summary line.
- src/CMakeLists.txt: dependency checks on
SECP256K1_ENABLE_MODULE_SCHNORRSIG and SECP256K1_ENABLE_MODULE_ECDH,
ENABLE_MODULE_CHILLDKG=1 compile definition, public header export.
Verified:
- ./autogen.sh && ./configure --enable-experimental
--enable-module-chilldkg --enable-module-schnorrsig
--enable-module-ecdh && make check: PASS 3/3 (tests, noverify_tests,
exhaustive_tests).
- configure fails with a clear error when schnorrsig or ecdh are
disabled, or when experimental is not enabled.
- CMake build with SECP256K1_ENABLE_MODULE_CHILLDKG=ON: ctest 345/345
passed; dependency errors fire correctly when schnorrsig/ecdh OFF.
2026-08-31 01:37:22 +02:00
|
|
|
#ifndef SECP256K1_CHILLDKG_H
|
|
|
|
|
#define SECP256K1_CHILLDKG_H
|
|
|
|
|
|
|
|
|
|
#include "secp256k1.h"
|
|
|
|
|
#include "secp256k1_extrakeys.h"
|
|
|
|
|
|
|
|
|
|
#ifdef __cplusplus
|
|
|
|
|
extern "C" {
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
#include <stddef.h>
|
|
|
|
|
#include <stdint.h>
|
|
|
|
|
|
|
|
|
|
/** This module implements ChillDKG, a distributed key generation (DKG)
|
|
|
|
|
* protocol for FROST, as specified by the bip-frost-dkg BIP draft
|
|
|
|
|
* (https://github.com/BlockstreamResearch/bip-frost-dkg, version 0.3.0-dev).
|
|
|
|
|
*
|
|
|
|
|
* This code is currently a work in progress. It's not secure nor stable.
|
|
|
|
|
* IT IS EXTREMELY DANGEROUS AND RECKLESS TO USE THIS MODULE IN PRODUCTION!
|
|
|
|
|
*
|
|
|
|
|
* Moreover, the bip-frost-dkg BIP is still a draft: tagged hashes, wire
|
|
|
|
|
* formats, and protocol details may change in future BIP versions. There is
|
|
|
|
|
* no guarantee that this implementation will remain compatible with the
|
|
|
|
|
* final specification.
|
|
|
|
|
*
|
|
|
|
|
* The output of a ChillDKG session (a secret share, the threshold public
|
|
|
|
|
* key, and the public shares of all participants) is designed to be used
|
|
|
|
|
* directly with the FROST signing module (see include/secp256k1_frost.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
|
|
|
* A DKG session involves n participants (identified by uint32 identifiers
|
|
|
|
|
* 0..n-1) and an untrusted coordinator. The number of participants n must
|
|
|
|
|
* not exceed SECP256K1_CHILLDKG_MAX_PARTICIPANTS. The message flow is:
|
|
|
|
|
* 1. Every participant runs secp256k1_chilldkg_participant_step1 and sends
|
|
|
|
|
* the resulting pmsg1 to the coordinator.
|
chilldkg: Phase 4 - public coordinator API
Add the coordinator side of the ChillDKG protocol
(bip-frost-dkg v0.3.0-dev, reference commit
a91896883f85b159415ecf298d5e844879af112d), as thin wrappers over the
Phase 2 encpedpop coordinator internals (whose cmsg1 output was
already verified byte-identical to the reference coordinator_step1).
Public API:
- secp256k1_chilldkg_coordinator_step1: takes an array of pointers to
the n participant pmsg1 messages (musig/frost-style convention),
parses each with checked scalar parse, aggregates SimplPedPop and
EncPedPop, builds eq_input (including the enc_secshares suffix,
mirroring the reference) and emits cmsg1 (162n + 33(t-1) bytes).
PoPs are not verified coordinator-side, exactly as the reference.
- secp256k1_chilldkg_coordinator_finalize: concatenates the n CertEq
pmsg2 signatures into the 64n-byte certificate, verifies all of them
via certeq_verify (hostpubkeys recovered from eq_input at offset
4+33t), and outputs the coordinator-side DKG result: threshold
pubkey, pubshares and recovery data -- no secshare.
- secp256k1_chilldkg_coordinator_state: opaque, 21041 bytes,
magic-validated, holds only t, n, eq_input, thresh_pk and pubshares
-- no secret material, documented as freely copyable/persistable so
a stateless coordinator is possible.
Blame mapping (verified against chilldkg.py):
- malformed pmsg1 (bad commitment encoding, overflowing encrypted
share) -> FAULTY_PARTICIPANT(sender index),
- invalid CertEq signature -> FAULTY_PARTICIPANT(failing index)
(deliberately different from participant_finalize, which maps the
same failure to FAULTY_COORDINATOR -- matching the reference),
- invalid session params -> INVALID_INPUT; all outputs zeroed on
failure.
tests_impl.h: chilldkg_coordinator_api_test runs a full n=3,t=2
session through only the public APIs on both sides, byte-exact
against the Python reference vectors and cross-checked against every
participant's finalize outputs; blame cases (malformed pmsg1 and
overflowing share -> FAULTY_PARTICIPANT with the right index, bad
CertEq sig -> FAULTY_PARTICIPANT(2), zeroed outputs); misuse coverage
(NULL args, corrupted state magic).
Verified: make check 3/3 (incl. noverify); CMake ctest 365/365;
./tests --target=chilldkg green.
2026-08-31 05:33:42 +02:00
|
|
|
* 2. The coordinator runs secp256k1_chilldkg_coordinator_step1 on all
|
|
|
|
|
* pmsg1s and broadcasts the resulting cmsg1 to all participants.
|
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
|
|
|
* 3. Every participant runs secp256k1_chilldkg_participant_step2 and sends
|
|
|
|
|
* the resulting signature (pmsg2) to the coordinator.
|
chilldkg: Phase 4 - public coordinator API
Add the coordinator side of the ChillDKG protocol
(bip-frost-dkg v0.3.0-dev, reference commit
a91896883f85b159415ecf298d5e844879af112d), as thin wrappers over the
Phase 2 encpedpop coordinator internals (whose cmsg1 output was
already verified byte-identical to the reference coordinator_step1).
Public API:
- secp256k1_chilldkg_coordinator_step1: takes an array of pointers to
the n participant pmsg1 messages (musig/frost-style convention),
parses each with checked scalar parse, aggregates SimplPedPop and
EncPedPop, builds eq_input (including the enc_secshares suffix,
mirroring the reference) and emits cmsg1 (162n + 33(t-1) bytes).
PoPs are not verified coordinator-side, exactly as the reference.
- secp256k1_chilldkg_coordinator_finalize: concatenates the n CertEq
pmsg2 signatures into the 64n-byte certificate, verifies all of them
via certeq_verify (hostpubkeys recovered from eq_input at offset
4+33t), and outputs the coordinator-side DKG result: threshold
pubkey, pubshares and recovery data -- no secshare.
- secp256k1_chilldkg_coordinator_state: opaque, 21041 bytes,
magic-validated, holds only t, n, eq_input, thresh_pk and pubshares
-- no secret material, documented as freely copyable/persistable so
a stateless coordinator is possible.
Blame mapping (verified against chilldkg.py):
- malformed pmsg1 (bad commitment encoding, overflowing encrypted
share) -> FAULTY_PARTICIPANT(sender index),
- invalid CertEq signature -> FAULTY_PARTICIPANT(failing index)
(deliberately different from participant_finalize, which maps the
same failure to FAULTY_COORDINATOR -- matching the reference),
- invalid session params -> INVALID_INPUT; all outputs zeroed on
failure.
tests_impl.h: chilldkg_coordinator_api_test runs a full n=3,t=2
session through only the public APIs on both sides, byte-exact
against the Python reference vectors and cross-checked against every
participant's finalize outputs; blame cases (malformed pmsg1 and
overflowing share -> FAULTY_PARTICIPANT with the right index, bad
CertEq sig -> FAULTY_PARTICIPANT(2), zeroed outputs); misuse coverage
(NULL args, corrupted state magic).
Verified: make check 3/3 (incl. noverify); CMake ctest 365/365;
./tests --target=chilldkg green.
2026-08-31 05:33:42 +02:00
|
|
|
* 4. The coordinator runs secp256k1_chilldkg_coordinator_finalize on all
|
|
|
|
|
* pmsg2s and broadcasts the resulting certificate (cmsg2) to all
|
|
|
|
|
* participants.
|
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
|
|
|
* 5. Every participant runs secp256k1_chilldkg_participant_finalize to
|
|
|
|
|
* obtain the DKG output and the recovery data.
|
|
|
|
|
*
|
chilldkg: Phase 0 - module scaffolding and build wiring
Add an empty, experimental `chilldkg` module as the foundation for a
ChillDKG implementation (distributed key generation for FROST) per the
bip-frost-dkg BIP draft (v0.3.0-dev):
https://github.com/BlockstreamResearch/bip-frost-dkg
The module lives in src/modules/chilldkg/ (separate from the frost
module, per the implementation plan in .idea/docs/
chilldkg-implementation-plan.md: FROST signing (BIP 445) and ChillDKG
are separate BIPs with separate reference repos, test vectors and
review cycles; the dependency between them is one-way bytes).
New files:
- include/secp256k1_chilldkg.h: public header skeleton with the same
"EXTREMELY DANGEROUS / work in progress" warning style as
secp256k1_frost.h, plus a note that the BIP is a draft and tagged
hashes/wire formats may change. No API yet (Phase 3+).
- src/modules/chilldkg/main_impl.h: implementation skeleton including
the public header.
- src/modules/chilldkg/tests_impl.h: trivial scaffolding unit test
(chilldkg_scaffolding_test) registered via the tests_chilldkg[]
CASE1 array used by this repo's unit-test framework.
- src/modules/chilldkg/Makefile.am.include: autotools file list,
mirroring the frost module's.
- src/modules/chilldkg/chilldkg.md: module doc stub (purpose, draft
status, dependency on the schnorrsig and ecdh modules).
Build wiring (mirrors the frost module exactly):
- configure.ac: --enable-module-chilldkg (default no, experimental
gate), dependency errors when schnorrsig or ecdh are explicitly
disabled, AM_CONDITIONAL(ENABLE_MODULE_CHILLDKG), summary line.
- Makefile.am: include src/modules/chilldkg/Makefile.am.include under
ENABLE_MODULE_CHILLDKG.
- src/secp256k1.c: guarded include of modules/chilldkg/main_impl.h
after the frost module.
- src/tests.c: guarded include of tests_impl.h and
MAKE_TEST_MODULE(chilldkg) registration.
- CMakeLists.txt: SECP256K1_ENABLE_MODULE_CHILLDKG option (OFF) +
summary line.
- src/CMakeLists.txt: dependency checks on
SECP256K1_ENABLE_MODULE_SCHNORRSIG and SECP256K1_ENABLE_MODULE_ECDH,
ENABLE_MODULE_CHILLDKG=1 compile definition, public header export.
Verified:
- ./autogen.sh && ./configure --enable-experimental
--enable-module-chilldkg --enable-module-schnorrsig
--enable-module-ecdh && make check: PASS 3/3 (tests, noverify_tests,
exhaustive_tests).
- configure fails with a clear error when schnorrsig or ecdh are
disabled, or when experimental is not enabled.
- CMake build with SECP256K1_ENABLE_MODULE_CHILLDKG=ON: ctest 345/345
passed; dependency errors fire correctly when schnorrsig/ecdh OFF.
2026-08-31 01:37:22 +02:00
|
|
|
* It is recommended to read the documentation in this include file carefully.
|
|
|
|
|
* Further notes on API usage can be found in src/modules/chilldkg/chilldkg.md.
|
|
|
|
|
*/
|
|
|
|
|
|
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 maximum number of participants n in a ChillDKG session. The state
|
|
|
|
|
* objects of this module are fixed-size and do not use dynamic allocation,
|
|
|
|
|
* so a compile-time cap is required. This matches the FROST module's
|
|
|
|
|
* SECP256K1_FROST_MAX_PARTICIPANTS. */
|
|
|
|
|
#define SECP256K1_CHILLDKG_MAX_PARTICIPANTS 128
|
|
|
|
|
|
|
|
|
|
/** Fault report of the ChillDKG protocol functions, mapping the exception
|
|
|
|
|
* taxonomy of the reference implementation.
|
|
|
|
|
*
|
|
|
|
|
* For SECP256K1_CHILLDKG_FAULTY_PARTICIPANT and
|
|
|
|
|
* SECP256K1_CHILLDKG_FAULTY_PARTICIPANT_OR_COORDINATOR, the fault_index
|
|
|
|
|
* output of the failing function is set to the identifier of the (suspected)
|
|
|
|
|
* faulty participant. For the other fault codes, fault_index is set to
|
|
|
|
|
* UINT32_MAX, except where documented otherwise. */
|
|
|
|
|
typedef enum {
|
|
|
|
|
/** No fault; the step succeeded. */
|
|
|
|
|
SECP256K1_CHILLDKG_OK = 0,
|
|
|
|
|
/** The coordinator is faulty. */
|
|
|
|
|
SECP256K1_CHILLDKG_FAULTY_COORDINATOR = 1,
|
|
|
|
|
/** The participant with the given fault_index is faulty. */
|
|
|
|
|
SECP256K1_CHILLDKG_FAULTY_PARTICIPANT = 2,
|
|
|
|
|
/** The participant with the given fault_index or the coordinator is
|
|
|
|
|
* faulty. */
|
|
|
|
|
SECP256K1_CHILLDKG_FAULTY_PARTICIPANT_OR_COORDINATOR = 3,
|
|
|
|
|
/** Some unknown participant or the coordinator is faulty; the
|
|
|
|
|
* investigation procedure of the protocol is necessary to determine a
|
|
|
|
|
* suspected participant. */
|
|
|
|
|
SECP256K1_CHILLDKG_UNKNOWN_FAULTY_PARTICIPANT_OR_COORDINATOR = 4,
|
|
|
|
|
/** The caller provided invalid input (e.g., an invalid host secret key or
|
|
|
|
|
* invalid session parameters). */
|
|
|
|
|
SECP256K1_CHILLDKG_INVALID_INPUT = 5
|
|
|
|
|
} secp256k1_chilldkg_fault;
|
|
|
|
|
|
|
|
|
|
/** Opaque data structures
|
|
|
|
|
*
|
|
|
|
|
* The exact representation of data inside the opaque data structures is
|
|
|
|
|
* implementation defined and not guaranteed to be portable between different
|
|
|
|
|
* platforms or versions. The data structures can be safely copied/moved.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/** Opaque data structure that holds a participant's session state after
|
|
|
|
|
* secp256k1_chilldkg_participant_step1.
|
|
|
|
|
*
|
|
|
|
|
* The state does not contain secret key material (the secret shares it
|
|
|
|
|
* relates to are encrypted in pmsg1), but it must not be reused: it must be
|
|
|
|
|
* passed only to a single secp256k1_chilldkg_participant_step2 call.
|
|
|
|
|
*
|
|
|
|
|
* Guaranteed to be 4306 bytes in size.
|
|
|
|
|
*/
|
|
|
|
|
typedef struct secp256k1_chilldkg_participant_state1 {
|
|
|
|
|
unsigned char data[4 + 12 + 33 + 33 + 33 * SECP256K1_CHILLDKG_MAX_PARTICIPANTS];
|
|
|
|
|
} secp256k1_chilldkg_participant_state1;
|
|
|
|
|
|
|
|
|
|
/** Opaque data structure that holds a participant's session state after
|
|
|
|
|
* secp256k1_chilldkg_participant_step2.
|
|
|
|
|
*
|
|
|
|
|
* This structure contains the participant's secret share; it MUST be kept
|
|
|
|
|
* secret and MUST NOT be copied. It must not be reused: it must be passed
|
|
|
|
|
* only to a single secp256k1_chilldkg_participant_finalize call.
|
|
|
|
|
*
|
|
|
|
|
* Guaranteed to be 21073 bytes in size.
|
|
|
|
|
*/
|
|
|
|
|
typedef struct secp256k1_chilldkg_participant_state2 {
|
|
|
|
|
unsigned char data[12 + 4 + 131 * SECP256K1_CHILLDKG_MAX_PARTICIPANTS + 32 + 33 + 33 * SECP256K1_CHILLDKG_MAX_PARTICIPANTS];
|
|
|
|
|
} secp256k1_chilldkg_participant_state2;
|
|
|
|
|
|
chilldkg: Phase 5 - recovery, recovery acks, investigation
Complete the ChillDKG protocol surface with the recovery and blame-
attribution procedures (bip-frost-dkg v0.3.0-dev, reference commit
a91896883f85b159415ecf298d5e844879af112d).
Recovery:
- secp256k1_chilldkg_participant_recover / _coordinator_recover: parse
the self-delimiting recovery layout u32be(t) || sum_coms(33t) ||
hostpubkeys(33n) || pubnonces(33n) || enc_secshares(32n, checked) ||
cert(64n), deriving n = (len-4-33t)/162 exactly as the reference's
deserialize_recovery_data; re-verify the certificate, recompute the
TapTweak and the receiver's ECDH/self pads from (hostseckey,
pubnonces, enc_context), recompute the tweaked secshare, and
sanity-check secshare*G == pubshares[own]. Also return hostpubkeys,
n and t so callers can re-derive session params. RecoveryDataError /
HostSeckeyError / params failures map to INVALID_INPUT (no index,
as in the reference); an invalid pubnonce during decrypt passes
through as FAULTY_PARTICIPANT_OR_COORDINATOR(i), matching the
reference leaking that exception from recover().
Recovery acks:
- secp256k1_chilldkg_recovery_ack_sign / _acks_verify: BIP-340
(standard BIP0340 tags) over pad33("BIP DKG/recovery acknowledgment")
|| u32be(i) || recovery_data. Verification failure maps to
FAULTY_PARTICIPANT(i) (InvalidRecoveryAckError subclasses
FaultyParticipantError in the reference).
Investigation:
- secp256k1_chilldkg_coordinator_investigate: builds one 65n-byte
per-participant message (per-dealer encrypted partial secshares
(32n) + partial pubshares (33n)) per call; the reference returns all
n at once -- equivalent, the caller iterates.
- secp256k1_chilldkg_participant_investigate: the reference's
three-step blame attribution -- sum-of-pubshares check ->
FAULTY_COORDINATOR; sum-of-secshares check -> FAULTY_COORDINATOR
(covers the reference's SecshareSumError translation); per-dealer
decrypted share vs commitment -> FAULTY_PARTICIPANT_OR_COORDINATOR(i)
(or FAULTY_COORDINATOR for the own index); all-consistent ->
INVALID_INPUT (the reference's uncaught RuntimeError).
- Investigation data is transported via a new opaque, secret-bearing
secp256k1_chilldkg_participant_inv_data object (4205 bytes,
magic-validated save/load, secret-cleared) filled by
participant_step2 on the UNKNOWN_FAULT paths. This amends the
Phase 3 participant_step2 signature with a nullable inv_data
out-param -- required because recomputing inside
participant_investigate would duplicate step2's decrypt/verify
logic.
- New length helper secp256k1_chilldkg_investigation_msg_len (65n).
tests_impl.h: chilldkg_recovery_test (recover roundtrips byte-exact
vs the session outputs and reference vectors, tampered/truncated/
over-long recovery data, unknown/invalid hostseckey, ack sign
byte-exact + verify with wrong-index and tampered-ack blame,
params/recovery mismatch rejects, misuse) and
chilldkg_investigate_test (two end-to-end public-API scenarios
generated from the reference: dealer corrupting a participant's
encrypted share, and coordinator tampering with an enc_secshare;
cmsg1/cinv/inv_data byte-exact, blame codes and indices matching the
reference's exception type and index; malformed cinv ->
FAULTY_COORDINATOR; malformed pmsg1 -> FAULTY_PARTICIPANT(j); misuse).
The all-consistent investigate path is not constructible without
discrete logs and matches the reference's unreachable RuntimeError.
Verified: make check 3/3 (incl. noverify); CMake ctest 369/369;
make distdir clean.
2026-08-31 06:09:03 +02:00
|
|
|
/** Opaque data structure that holds the investigation data output by
|
|
|
|
|
* secp256k1_chilldkg_participant_step2 when it returns
|
|
|
|
|
* SECP256K1_CHILLDKG_UNKNOWN_FAULTY_PARTICIPANT_OR_COORDINATOR, to be passed
|
|
|
|
|
* to secp256k1_chilldkg_participant_investigate together with the
|
|
|
|
|
* coordinator's investigation message.
|
|
|
|
|
*
|
|
|
|
|
* This structure contains secret data (the untweaked secret share and the
|
|
|
|
|
* decryption pads); it MUST be kept secret.
|
|
|
|
|
*
|
|
|
|
|
* Guaranteed to be 4205 bytes in size.
|
|
|
|
|
*/
|
|
|
|
|
typedef struct secp256k1_chilldkg_participant_inv_data {
|
|
|
|
|
unsigned char data[12 + 32 + 33 + 32 + 32 * SECP256K1_CHILLDKG_MAX_PARTICIPANTS];
|
|
|
|
|
} secp256k1_chilldkg_participant_inv_data;
|
|
|
|
|
|
chilldkg: Phase 4 - public coordinator API
Add the coordinator side of the ChillDKG protocol
(bip-frost-dkg v0.3.0-dev, reference commit
a91896883f85b159415ecf298d5e844879af112d), as thin wrappers over the
Phase 2 encpedpop coordinator internals (whose cmsg1 output was
already verified byte-identical to the reference coordinator_step1).
Public API:
- secp256k1_chilldkg_coordinator_step1: takes an array of pointers to
the n participant pmsg1 messages (musig/frost-style convention),
parses each with checked scalar parse, aggregates SimplPedPop and
EncPedPop, builds eq_input (including the enc_secshares suffix,
mirroring the reference) and emits cmsg1 (162n + 33(t-1) bytes).
PoPs are not verified coordinator-side, exactly as the reference.
- secp256k1_chilldkg_coordinator_finalize: concatenates the n CertEq
pmsg2 signatures into the 64n-byte certificate, verifies all of them
via certeq_verify (hostpubkeys recovered from eq_input at offset
4+33t), and outputs the coordinator-side DKG result: threshold
pubkey, pubshares and recovery data -- no secshare.
- secp256k1_chilldkg_coordinator_state: opaque, 21041 bytes,
magic-validated, holds only t, n, eq_input, thresh_pk and pubshares
-- no secret material, documented as freely copyable/persistable so
a stateless coordinator is possible.
Blame mapping (verified against chilldkg.py):
- malformed pmsg1 (bad commitment encoding, overflowing encrypted
share) -> FAULTY_PARTICIPANT(sender index),
- invalid CertEq signature -> FAULTY_PARTICIPANT(failing index)
(deliberately different from participant_finalize, which maps the
same failure to FAULTY_COORDINATOR -- matching the reference),
- invalid session params -> INVALID_INPUT; all outputs zeroed on
failure.
tests_impl.h: chilldkg_coordinator_api_test runs a full n=3,t=2
session through only the public APIs on both sides, byte-exact
against the Python reference vectors and cross-checked against every
participant's finalize outputs; blame cases (malformed pmsg1 and
overflowing share -> FAULTY_PARTICIPANT with the right index, bad
CertEq sig -> FAULTY_PARTICIPANT(2), zeroed outputs); misuse coverage
(NULL args, corrupted state magic).
Verified: make check 3/3 (incl. noverify); CMake ctest 365/365;
./tests --target=chilldkg green.
2026-08-31 05:33:42 +02:00
|
|
|
/** Opaque data structure that holds the coordinator's session state after
|
|
|
|
|
* secp256k1_chilldkg_coordinator_step1, to be passed to
|
|
|
|
|
* secp256k1_chilldkg_coordinator_finalize (it must not be reused).
|
|
|
|
|
*
|
|
|
|
|
* This structure contains no secret key material; it can be copied freely
|
|
|
|
|
* (e.g., to persist it between the two coordinator steps).
|
|
|
|
|
*
|
|
|
|
|
* Guaranteed to be 21041 bytes in size.
|
|
|
|
|
*/
|
|
|
|
|
typedef struct secp256k1_chilldkg_coordinator_state {
|
|
|
|
|
unsigned char data[12 + 4 + 131 * SECP256K1_CHILLDKG_MAX_PARTICIPANTS + 33 + 33 * SECP256K1_CHILLDKG_MAX_PARTICIPANTS];
|
|
|
|
|
} secp256k1_chilldkg_coordinator_state;
|
|
|
|
|
|
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
|
|
|
/** Compute the participant's host public key from the host secret key.
|
|
|
|
|
*
|
|
|
|
|
* The host public key is the long-term cryptographic identity of the
|
|
|
|
|
* participant. This function interprets hostseckey32 as a big-endian integer
|
|
|
|
|
* and computes the corresponding "plain" public key in compressed
|
|
|
|
|
* serialization (33 bytes, starting with 0x02 or 0x03), equivalent to
|
|
|
|
|
* IndividualPubkey as defined in BIP 327.
|
|
|
|
|
*
|
|
|
|
|
* Returns: 1 on success, 0 if the host secret key is invalid (zero or not
|
|
|
|
|
* less than the group order). On failure, hostpubkey33 is set to
|
|
|
|
|
* zero.
|
|
|
|
|
* Args: ctx: pointer to a context object
|
|
|
|
|
* Out: hostpubkey33: pointer to a 33-byte array to store the host public key
|
|
|
|
|
* In: hostseckey32: pointer to the 32-byte host secret key
|
|
|
|
|
*/
|
|
|
|
|
SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_chilldkg_hostpubkey_gen(
|
|
|
|
|
const secp256k1_context *ctx,
|
|
|
|
|
unsigned char *hostpubkey33,
|
|
|
|
|
const unsigned char *hostseckey32
|
|
|
|
|
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3);
|
|
|
|
|
|
|
|
|
|
/** Return a hash of the session parameters for out-of-band comparison.
|
|
|
|
|
*
|
|
|
|
|
* If all participants have obtained an identical parameters hash (as can be
|
|
|
|
|
* verified out of band), then they all agree on all host public keys and the
|
|
|
|
|
* threshold t.
|
|
|
|
|
*
|
|
|
|
|
* Returns: 1 on success, 0 if the session parameters are invalid (not
|
|
|
|
|
* 1 <= t <= n <= SECP256K1_CHILLDKG_MAX_PARTICIPANTS, an invalid
|
|
|
|
|
* host public key, or a duplicate host public key). On failure,
|
|
|
|
|
* hash32 is set to zero.
|
|
|
|
|
* Args: ctx: pointer to a context object
|
|
|
|
|
* Out: hash32: pointer to a 32-byte array to store the parameters
|
|
|
|
|
* hash
|
|
|
|
|
* In: hostpubkeys33: pointer to an array of n_participants host public
|
|
|
|
|
* keys (33 bytes each, compressed serialization)
|
|
|
|
|
* n_participants: total number of participants n
|
|
|
|
|
* threshold: threshold t
|
|
|
|
|
*/
|
|
|
|
|
SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_chilldkg_params_hash(
|
|
|
|
|
const secp256k1_context *ctx,
|
|
|
|
|
unsigned char *hash32,
|
|
|
|
|
const unsigned char *hostpubkeys33,
|
|
|
|
|
size_t n_participants,
|
|
|
|
|
uint32_t threshold
|
|
|
|
|
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3);
|
|
|
|
|
|
|
|
|
|
/** Length of a participant's first message (pmsg1): 33*t + 32*n + 97 bytes.
|
|
|
|
|
* Returns 0 if the parameters are out of range. */
|
|
|
|
|
SECP256K1_API size_t secp256k1_chilldkg_participant_msg1_len(
|
|
|
|
|
size_t n_participants,
|
|
|
|
|
uint32_t threshold
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
/** Length of the coordinator's first message (cmsg1): 162*n + 33*(t-1) bytes.
|
|
|
|
|
* Returns 0 if the parameters are out of range. */
|
|
|
|
|
SECP256K1_API size_t secp256k1_chilldkg_coordinator_msg1_len(
|
|
|
|
|
size_t n_participants,
|
|
|
|
|
uint32_t threshold
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
/** Length of a participant's second message (pmsg2): 64 bytes. */
|
|
|
|
|
SECP256K1_API size_t secp256k1_chilldkg_participant_msg2_len(void);
|
|
|
|
|
|
|
|
|
|
/** Length of the coordinator's second message (cmsg2, the certificate):
|
|
|
|
|
* 64*n bytes. Returns 0 if the parameters are out of range. */
|
|
|
|
|
SECP256K1_API size_t secp256k1_chilldkg_coordinator_msg2_len(
|
|
|
|
|
size_t n_participants
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
/** Length of the recovery data output by
|
|
|
|
|
* secp256k1_chilldkg_participant_finalize: 4 + 33*t + 162*n bytes.
|
|
|
|
|
* Returns 0 if the parameters are out of range. */
|
|
|
|
|
SECP256K1_API size_t secp256k1_chilldkg_recovery_data_len(
|
|
|
|
|
size_t n_participants,
|
|
|
|
|
uint32_t threshold
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
/** Perform a participant's first step of a ChillDKG session.
|
|
|
|
|
*
|
|
|
|
|
* Returns: 1 on success, 0 on invalid input (invalid host secret key, host
|
|
|
|
|
* secret key not matching any host public key, invalid session
|
|
|
|
|
* parameters, or all-zero randomness). On failure, pmsg1 and the
|
|
|
|
|
* state are set to zero.
|
|
|
|
|
* Args: ctx: pointer to a context object
|
|
|
|
|
* Out: state1: pointer to a state1 object to be passed to
|
|
|
|
|
* secp256k1_chilldkg_participant_step2 (must not be
|
|
|
|
|
* reused)
|
|
|
|
|
* pmsg1: pointer to a 33*t + 32*n + 97 byte array (see
|
|
|
|
|
* secp256k1_chilldkg_participant_msg1_len) to store
|
|
|
|
|
* the message to be sent to the coordinator
|
|
|
|
|
* In: hostseckey32: pointer to the 32-byte host secret key
|
|
|
|
|
* hostpubkeys33: pointer to an array of n host public keys (33 bytes
|
|
|
|
|
* each); all participants must agree on the order
|
|
|
|
|
* n_participants: total number of participants n
|
|
|
|
|
* threshold: threshold t
|
|
|
|
|
* random32: pointer to 32 bytes of FRESH randomness
|
|
|
|
|
*/
|
|
|
|
|
SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_chilldkg_participant_step1(
|
|
|
|
|
const secp256k1_context *ctx,
|
|
|
|
|
secp256k1_chilldkg_participant_state1 *state1,
|
|
|
|
|
unsigned char *pmsg1,
|
|
|
|
|
const unsigned char *hostseckey32,
|
|
|
|
|
const unsigned char *hostpubkeys33,
|
|
|
|
|
size_t n_participants,
|
|
|
|
|
uint32_t threshold,
|
|
|
|
|
const unsigned char *random32
|
|
|
|
|
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4) SECP256K1_ARG_NONNULL(5) SECP256K1_ARG_NONNULL(8);
|
|
|
|
|
|
|
|
|
|
/** Perform a participant's second step of a ChillDKG session.
|
|
|
|
|
*
|
|
|
|
|
* Verifies the coordinator's first message, computes the DKG output, and
|
|
|
|
|
* produces the CertEq signature over the session transcript.
|
|
|
|
|
*
|
|
|
|
|
* **Warning:** After sending the produced signature to the coordinator, the
|
|
|
|
|
* caller **must not** erase the hostseckey, even if the coordinator reply
|
|
|
|
|
* needed for secp256k1_chilldkg_participant_finalize is not received (some
|
|
|
|
|
* other participant may deem the session successful and use the resulting
|
|
|
|
|
* threshold public key).
|
|
|
|
|
*
|
|
|
|
|
* Returns: SECP256K1_CHILLDKG_OK on success, otherwise a fault code:
|
|
|
|
|
* SECP256K1_CHILLDKG_INVALID_INPUT if the host secret key is
|
|
|
|
|
* invalid or does not match the one used in step 1;
|
|
|
|
|
* SECP256K1_CHILLDKG_FAULTY_COORDINATOR,
|
|
|
|
|
* SECP256K1_CHILLDKG_FAULTY_PARTICIPANT_OR_COORDINATOR, or
|
|
|
|
|
* SECP256K1_CHILLDKG_UNKNOWN_FAULTY_PARTICIPANT_OR_COORDINATOR on
|
|
|
|
|
* protocol faults. On failure, sig64 and the state are set to zero.
|
|
|
|
|
* Args: ctx: pointer to a context object
|
|
|
|
|
* Out: state2: pointer to a state2 object to be passed to
|
|
|
|
|
* secp256k1_chilldkg_participant_finalize (must not be
|
|
|
|
|
* reused)
|
|
|
|
|
* sig64: pointer to a 64-byte array to store the CertEq
|
|
|
|
|
* signature (pmsg2) to be sent to the coordinator
|
|
|
|
|
* fault_index: pointer to a uint32 that receives the identifier of
|
|
|
|
|
* the (suspected) faulty participant where applicable,
|
|
|
|
|
* and UINT32_MAX otherwise
|
chilldkg: Phase 5 - recovery, recovery acks, investigation
Complete the ChillDKG protocol surface with the recovery and blame-
attribution procedures (bip-frost-dkg v0.3.0-dev, reference commit
a91896883f85b159415ecf298d5e844879af112d).
Recovery:
- secp256k1_chilldkg_participant_recover / _coordinator_recover: parse
the self-delimiting recovery layout u32be(t) || sum_coms(33t) ||
hostpubkeys(33n) || pubnonces(33n) || enc_secshares(32n, checked) ||
cert(64n), deriving n = (len-4-33t)/162 exactly as the reference's
deserialize_recovery_data; re-verify the certificate, recompute the
TapTweak and the receiver's ECDH/self pads from (hostseckey,
pubnonces, enc_context), recompute the tweaked secshare, and
sanity-check secshare*G == pubshares[own]. Also return hostpubkeys,
n and t so callers can re-derive session params. RecoveryDataError /
HostSeckeyError / params failures map to INVALID_INPUT (no index,
as in the reference); an invalid pubnonce during decrypt passes
through as FAULTY_PARTICIPANT_OR_COORDINATOR(i), matching the
reference leaking that exception from recover().
Recovery acks:
- secp256k1_chilldkg_recovery_ack_sign / _acks_verify: BIP-340
(standard BIP0340 tags) over pad33("BIP DKG/recovery acknowledgment")
|| u32be(i) || recovery_data. Verification failure maps to
FAULTY_PARTICIPANT(i) (InvalidRecoveryAckError subclasses
FaultyParticipantError in the reference).
Investigation:
- secp256k1_chilldkg_coordinator_investigate: builds one 65n-byte
per-participant message (per-dealer encrypted partial secshares
(32n) + partial pubshares (33n)) per call; the reference returns all
n at once -- equivalent, the caller iterates.
- secp256k1_chilldkg_participant_investigate: the reference's
three-step blame attribution -- sum-of-pubshares check ->
FAULTY_COORDINATOR; sum-of-secshares check -> FAULTY_COORDINATOR
(covers the reference's SecshareSumError translation); per-dealer
decrypted share vs commitment -> FAULTY_PARTICIPANT_OR_COORDINATOR(i)
(or FAULTY_COORDINATOR for the own index); all-consistent ->
INVALID_INPUT (the reference's uncaught RuntimeError).
- Investigation data is transported via a new opaque, secret-bearing
secp256k1_chilldkg_participant_inv_data object (4205 bytes,
magic-validated save/load, secret-cleared) filled by
participant_step2 on the UNKNOWN_FAULT paths. This amends the
Phase 3 participant_step2 signature with a nullable inv_data
out-param -- required because recomputing inside
participant_investigate would duplicate step2's decrypt/verify
logic.
- New length helper secp256k1_chilldkg_investigation_msg_len (65n).
tests_impl.h: chilldkg_recovery_test (recover roundtrips byte-exact
vs the session outputs and reference vectors, tampered/truncated/
over-long recovery data, unknown/invalid hostseckey, ack sign
byte-exact + verify with wrong-index and tampered-ack blame,
params/recovery mismatch rejects, misuse) and
chilldkg_investigate_test (two end-to-end public-API scenarios
generated from the reference: dealer corrupting a participant's
encrypted share, and coordinator tampering with an enc_secshare;
cmsg1/cinv/inv_data byte-exact, blame codes and indices matching the
reference's exception type and index; malformed cinv ->
FAULTY_COORDINATOR; malformed pmsg1 -> FAULTY_PARTICIPANT(j); misuse).
The all-consistent investigate path is not constructible without
discrete logs and matches the reference's unreachable RuntimeError.
Verified: make check 3/3 (incl. noverify); CMake ctest 369/369;
make distdir clean.
2026-08-31 06:09:03 +02:00
|
|
|
* inv_data: pointer to a participant_inv_data object that
|
|
|
|
|
* receives the investigation data if (and only if) the
|
|
|
|
|
* return value is
|
|
|
|
|
* SECP256K1_CHILLDKG_UNKNOWN_FAULTY_PARTICIPANT_OR_COORDINATOR,
|
|
|
|
|
* or NULL if the caller does not intend to run
|
|
|
|
|
* secp256k1_chilldkg_participant_investigate
|
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
|
|
|
* In: state1: pointer to the state1 object output by
|
|
|
|
|
* secp256k1_chilldkg_participant_step1
|
|
|
|
|
* hostseckey32: pointer to the 32-byte host secret key (must be the
|
|
|
|
|
* same as in step 1)
|
|
|
|
|
* cmsg1: pointer to the coordinator's first message
|
|
|
|
|
* (162*n + 33*(t-1) bytes, see
|
|
|
|
|
* secp256k1_chilldkg_coordinator_msg1_len)
|
|
|
|
|
* aux_rand32: pointer to 32 bytes of auxiliary randomness for the
|
|
|
|
|
* CertEq signature (see BIP 340)
|
|
|
|
|
*/
|
|
|
|
|
SECP256K1_API SECP256K1_WARN_UNUSED_RESULT secp256k1_chilldkg_fault secp256k1_chilldkg_participant_step2(
|
|
|
|
|
const secp256k1_context *ctx,
|
|
|
|
|
secp256k1_chilldkg_participant_state2 *state2,
|
|
|
|
|
unsigned char *sig64,
|
|
|
|
|
uint32_t *fault_index,
|
chilldkg: Phase 5 - recovery, recovery acks, investigation
Complete the ChillDKG protocol surface with the recovery and blame-
attribution procedures (bip-frost-dkg v0.3.0-dev, reference commit
a91896883f85b159415ecf298d5e844879af112d).
Recovery:
- secp256k1_chilldkg_participant_recover / _coordinator_recover: parse
the self-delimiting recovery layout u32be(t) || sum_coms(33t) ||
hostpubkeys(33n) || pubnonces(33n) || enc_secshares(32n, checked) ||
cert(64n), deriving n = (len-4-33t)/162 exactly as the reference's
deserialize_recovery_data; re-verify the certificate, recompute the
TapTweak and the receiver's ECDH/self pads from (hostseckey,
pubnonces, enc_context), recompute the tweaked secshare, and
sanity-check secshare*G == pubshares[own]. Also return hostpubkeys,
n and t so callers can re-derive session params. RecoveryDataError /
HostSeckeyError / params failures map to INVALID_INPUT (no index,
as in the reference); an invalid pubnonce during decrypt passes
through as FAULTY_PARTICIPANT_OR_COORDINATOR(i), matching the
reference leaking that exception from recover().
Recovery acks:
- secp256k1_chilldkg_recovery_ack_sign / _acks_verify: BIP-340
(standard BIP0340 tags) over pad33("BIP DKG/recovery acknowledgment")
|| u32be(i) || recovery_data. Verification failure maps to
FAULTY_PARTICIPANT(i) (InvalidRecoveryAckError subclasses
FaultyParticipantError in the reference).
Investigation:
- secp256k1_chilldkg_coordinator_investigate: builds one 65n-byte
per-participant message (per-dealer encrypted partial secshares
(32n) + partial pubshares (33n)) per call; the reference returns all
n at once -- equivalent, the caller iterates.
- secp256k1_chilldkg_participant_investigate: the reference's
three-step blame attribution -- sum-of-pubshares check ->
FAULTY_COORDINATOR; sum-of-secshares check -> FAULTY_COORDINATOR
(covers the reference's SecshareSumError translation); per-dealer
decrypted share vs commitment -> FAULTY_PARTICIPANT_OR_COORDINATOR(i)
(or FAULTY_COORDINATOR for the own index); all-consistent ->
INVALID_INPUT (the reference's uncaught RuntimeError).
- Investigation data is transported via a new opaque, secret-bearing
secp256k1_chilldkg_participant_inv_data object (4205 bytes,
magic-validated save/load, secret-cleared) filled by
participant_step2 on the UNKNOWN_FAULT paths. This amends the
Phase 3 participant_step2 signature with a nullable inv_data
out-param -- required because recomputing inside
participant_investigate would duplicate step2's decrypt/verify
logic.
- New length helper secp256k1_chilldkg_investigation_msg_len (65n).
tests_impl.h: chilldkg_recovery_test (recover roundtrips byte-exact
vs the session outputs and reference vectors, tampered/truncated/
over-long recovery data, unknown/invalid hostseckey, ack sign
byte-exact + verify with wrong-index and tampered-ack blame,
params/recovery mismatch rejects, misuse) and
chilldkg_investigate_test (two end-to-end public-API scenarios
generated from the reference: dealer corrupting a participant's
encrypted share, and coordinator tampering with an enc_secshare;
cmsg1/cinv/inv_data byte-exact, blame codes and indices matching the
reference's exception type and index; malformed cinv ->
FAULTY_COORDINATOR; malformed pmsg1 -> FAULTY_PARTICIPANT(j); misuse).
The all-consistent investigate path is not constructible without
discrete logs and matches the reference's unreachable RuntimeError.
Verified: make check 3/3 (incl. noverify); CMake ctest 369/369;
make distdir clean.
2026-08-31 06:09:03 +02:00
|
|
|
secp256k1_chilldkg_participant_inv_data *inv_data,
|
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
|
|
|
const secp256k1_chilldkg_participant_state1 *state1,
|
|
|
|
|
const unsigned char *hostseckey32,
|
|
|
|
|
const unsigned char *cmsg1,
|
|
|
|
|
const unsigned char *aux_rand32
|
chilldkg: Phase 5 - recovery, recovery acks, investigation
Complete the ChillDKG protocol surface with the recovery and blame-
attribution procedures (bip-frost-dkg v0.3.0-dev, reference commit
a91896883f85b159415ecf298d5e844879af112d).
Recovery:
- secp256k1_chilldkg_participant_recover / _coordinator_recover: parse
the self-delimiting recovery layout u32be(t) || sum_coms(33t) ||
hostpubkeys(33n) || pubnonces(33n) || enc_secshares(32n, checked) ||
cert(64n), deriving n = (len-4-33t)/162 exactly as the reference's
deserialize_recovery_data; re-verify the certificate, recompute the
TapTweak and the receiver's ECDH/self pads from (hostseckey,
pubnonces, enc_context), recompute the tweaked secshare, and
sanity-check secshare*G == pubshares[own]. Also return hostpubkeys,
n and t so callers can re-derive session params. RecoveryDataError /
HostSeckeyError / params failures map to INVALID_INPUT (no index,
as in the reference); an invalid pubnonce during decrypt passes
through as FAULTY_PARTICIPANT_OR_COORDINATOR(i), matching the
reference leaking that exception from recover().
Recovery acks:
- secp256k1_chilldkg_recovery_ack_sign / _acks_verify: BIP-340
(standard BIP0340 tags) over pad33("BIP DKG/recovery acknowledgment")
|| u32be(i) || recovery_data. Verification failure maps to
FAULTY_PARTICIPANT(i) (InvalidRecoveryAckError subclasses
FaultyParticipantError in the reference).
Investigation:
- secp256k1_chilldkg_coordinator_investigate: builds one 65n-byte
per-participant message (per-dealer encrypted partial secshares
(32n) + partial pubshares (33n)) per call; the reference returns all
n at once -- equivalent, the caller iterates.
- secp256k1_chilldkg_participant_investigate: the reference's
three-step blame attribution -- sum-of-pubshares check ->
FAULTY_COORDINATOR; sum-of-secshares check -> FAULTY_COORDINATOR
(covers the reference's SecshareSumError translation); per-dealer
decrypted share vs commitment -> FAULTY_PARTICIPANT_OR_COORDINATOR(i)
(or FAULTY_COORDINATOR for the own index); all-consistent ->
INVALID_INPUT (the reference's uncaught RuntimeError).
- Investigation data is transported via a new opaque, secret-bearing
secp256k1_chilldkg_participant_inv_data object (4205 bytes,
magic-validated save/load, secret-cleared) filled by
participant_step2 on the UNKNOWN_FAULT paths. This amends the
Phase 3 participant_step2 signature with a nullable inv_data
out-param -- required because recomputing inside
participant_investigate would duplicate step2's decrypt/verify
logic.
- New length helper secp256k1_chilldkg_investigation_msg_len (65n).
tests_impl.h: chilldkg_recovery_test (recover roundtrips byte-exact
vs the session outputs and reference vectors, tampered/truncated/
over-long recovery data, unknown/invalid hostseckey, ack sign
byte-exact + verify with wrong-index and tampered-ack blame,
params/recovery mismatch rejects, misuse) and
chilldkg_investigate_test (two end-to-end public-API scenarios
generated from the reference: dealer corrupting a participant's
encrypted share, and coordinator tampering with an enc_secshare;
cmsg1/cinv/inv_data byte-exact, blame codes and indices matching the
reference's exception type and index; malformed cinv ->
FAULTY_COORDINATOR; malformed pmsg1 -> FAULTY_PARTICIPANT(j); misuse).
The all-consistent investigate path is not constructible without
discrete logs and matches the reference's unreachable RuntimeError.
Verified: make check 3/3 (incl. noverify); CMake ctest 369/369;
make distdir clean.
2026-08-31 06:09:03 +02:00
|
|
|
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4) SECP256K1_ARG_NONNULL(6) SECP256K1_ARG_NONNULL(7) SECP256K1_ARG_NONNULL(8) SECP256K1_ARG_NONNULL(9);
|
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
|
|
|
|
|
|
|
|
/** Perform a participant's final step of a ChillDKG session.
|
|
|
|
|
*
|
|
|
|
|
* Re-verifies all n CertEq signatures of the certificate and outputs the
|
|
|
|
|
* DKG output and the recovery data. If this function returns
|
|
|
|
|
* SECP256K1_CHILLDKG_OK, this participant deems the DKG session successful.
|
|
|
|
|
*
|
|
|
|
|
* Returns: SECP256K1_CHILLDKG_OK on success,
|
|
|
|
|
* SECP256K1_CHILLDKG_FAULTY_COORDINATOR if the certificate contains
|
|
|
|
|
* an invalid signature. As diagnostic information (deviating from
|
|
|
|
|
* the reference implementation, which does not report it),
|
|
|
|
|
* fault_index receives the index of the first invalid signature in
|
|
|
|
|
* the latter case. On failure, all outputs are set to zero.
|
|
|
|
|
* Args: ctx: pointer to a context object
|
|
|
|
|
* Out: secshare32: pointer to a 32-byte array to store the (tweaked)
|
|
|
|
|
* secret share
|
|
|
|
|
* thresh_pk33: pointer to a 33-byte array to store the threshold
|
|
|
|
|
* public key (compressed serialization)
|
|
|
|
|
* pubshares33: pointer to an array of n 33-byte elements to store
|
|
|
|
|
* the public shares of all participants
|
|
|
|
|
* recovery: pointer to a 4 + 33*t + 162*n byte array (see
|
|
|
|
|
* secp256k1_chilldkg_recovery_data_len) to store the
|
|
|
|
|
* recovery data
|
|
|
|
|
* fault_index: pointer to a uint32 (see above)
|
|
|
|
|
* In: state2: pointer to the state2 object output by
|
|
|
|
|
* secp256k1_chilldkg_participant_step2
|
|
|
|
|
* cmsg2: pointer to the coordinator's second message (the
|
|
|
|
|
* certificate, 64*n bytes)
|
|
|
|
|
*/
|
|
|
|
|
SECP256K1_API SECP256K1_WARN_UNUSED_RESULT secp256k1_chilldkg_fault secp256k1_chilldkg_participant_finalize(
|
|
|
|
|
const secp256k1_context *ctx,
|
|
|
|
|
unsigned char *secshare32,
|
|
|
|
|
unsigned char *thresh_pk33,
|
|
|
|
|
unsigned char *pubshares33,
|
|
|
|
|
unsigned char *recovery,
|
|
|
|
|
uint32_t *fault_index,
|
|
|
|
|
const secp256k1_chilldkg_participant_state2 *state2,
|
|
|
|
|
const unsigned char *cmsg2
|
|
|
|
|
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4) SECP256K1_ARG_NONNULL(5) SECP256K1_ARG_NONNULL(6) SECP256K1_ARG_NONNULL(7) SECP256K1_ARG_NONNULL(8);
|
|
|
|
|
|
chilldkg: Phase 4 - public coordinator API
Add the coordinator side of the ChillDKG protocol
(bip-frost-dkg v0.3.0-dev, reference commit
a91896883f85b159415ecf298d5e844879af112d), as thin wrappers over the
Phase 2 encpedpop coordinator internals (whose cmsg1 output was
already verified byte-identical to the reference coordinator_step1).
Public API:
- secp256k1_chilldkg_coordinator_step1: takes an array of pointers to
the n participant pmsg1 messages (musig/frost-style convention),
parses each with checked scalar parse, aggregates SimplPedPop and
EncPedPop, builds eq_input (including the enc_secshares suffix,
mirroring the reference) and emits cmsg1 (162n + 33(t-1) bytes).
PoPs are not verified coordinator-side, exactly as the reference.
- secp256k1_chilldkg_coordinator_finalize: concatenates the n CertEq
pmsg2 signatures into the 64n-byte certificate, verifies all of them
via certeq_verify (hostpubkeys recovered from eq_input at offset
4+33t), and outputs the coordinator-side DKG result: threshold
pubkey, pubshares and recovery data -- no secshare.
- secp256k1_chilldkg_coordinator_state: opaque, 21041 bytes,
magic-validated, holds only t, n, eq_input, thresh_pk and pubshares
-- no secret material, documented as freely copyable/persistable so
a stateless coordinator is possible.
Blame mapping (verified against chilldkg.py):
- malformed pmsg1 (bad commitment encoding, overflowing encrypted
share) -> FAULTY_PARTICIPANT(sender index),
- invalid CertEq signature -> FAULTY_PARTICIPANT(failing index)
(deliberately different from participant_finalize, which maps the
same failure to FAULTY_COORDINATOR -- matching the reference),
- invalid session params -> INVALID_INPUT; all outputs zeroed on
failure.
tests_impl.h: chilldkg_coordinator_api_test runs a full n=3,t=2
session through only the public APIs on both sides, byte-exact
against the Python reference vectors and cross-checked against every
participant's finalize outputs; blame cases (malformed pmsg1 and
overflowing share -> FAULTY_PARTICIPANT with the right index, bad
CertEq sig -> FAULTY_PARTICIPANT(2), zeroed outputs); misuse coverage
(NULL args, corrupted state magic).
Verified: make check 3/3 (incl. noverify); CMake ctest 365/365;
./tests --target=chilldkg green.
2026-08-31 05:33:42 +02:00
|
|
|
/** Perform the coordinator's first step of a ChillDKG session.
|
|
|
|
|
*
|
|
|
|
|
* Parses all n participant messages and aggregates them into the message to
|
|
|
|
|
* broadcast to all participants. The proofs of possession contained in the
|
|
|
|
|
* pmsg1s are NOT verified here; the participants verify them in step 2 (this
|
|
|
|
|
* mirrors the reference implementation).
|
|
|
|
|
*
|
|
|
|
|
* Returns: SECP256K1_CHILLDKG_OK on success,
|
|
|
|
|
* SECP256K1_CHILLDKG_INVALID_INPUT on invalid session parameters,
|
|
|
|
|
* or SECP256K1_CHILLDKG_FAULTY_PARTICIPANT (with fault_index set to
|
|
|
|
|
* the sender) if a participant message is malformed (invalid
|
|
|
|
|
* commitment encoding, or an encrypted share that overflows the
|
|
|
|
|
* group order). On failure, cmsg1 and the state are set to zero.
|
|
|
|
|
* Args: ctx: pointer to a context object
|
|
|
|
|
* Out: state: pointer to a coordinator_state object to be passed
|
|
|
|
|
* to secp256k1_chilldkg_coordinator_finalize (must not
|
|
|
|
|
* be reused)
|
|
|
|
|
* cmsg1: pointer to a 162*n + 33*(t-1) byte array (see
|
|
|
|
|
* secp256k1_chilldkg_coordinator_msg1_len) to store
|
|
|
|
|
* the message to be broadcast to all participants
|
|
|
|
|
* fault_index: pointer to a uint32 that receives the identifier of
|
|
|
|
|
* the faulty participant where applicable, and
|
|
|
|
|
* UINT32_MAX otherwise
|
|
|
|
|
* In: pmsgs1: array of n pointers to the participants' first
|
|
|
|
|
* messages (33*t + 32*n + 97 bytes each)
|
|
|
|
|
* hostpubkeys33: pointer to an array of n host public keys (33 bytes
|
|
|
|
|
* each); must be identical (in content and order) to
|
|
|
|
|
* the arrays used by the participants
|
|
|
|
|
* n_participants: total number of participants n
|
|
|
|
|
* threshold: threshold t
|
|
|
|
|
*/
|
|
|
|
|
SECP256K1_API SECP256K1_WARN_UNUSED_RESULT secp256k1_chilldkg_fault secp256k1_chilldkg_coordinator_step1(
|
|
|
|
|
const secp256k1_context *ctx,
|
|
|
|
|
secp256k1_chilldkg_coordinator_state *state,
|
|
|
|
|
unsigned char *cmsg1,
|
|
|
|
|
uint32_t *fault_index,
|
|
|
|
|
const unsigned char *const *pmsgs1,
|
|
|
|
|
const unsigned char *hostpubkeys33,
|
|
|
|
|
size_t n_participants,
|
|
|
|
|
uint32_t threshold
|
|
|
|
|
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4) SECP256K1_ARG_NONNULL(5) SECP256K1_ARG_NONNULL(6);
|
|
|
|
|
|
|
|
|
|
/** Perform the coordinator's final step of a ChillDKG session.
|
|
|
|
|
*
|
|
|
|
|
* Collects the n CertEq signatures into the certificate and verifies all of
|
|
|
|
|
* them. If this function returns SECP256K1_CHILLDKG_OK, the coordinator
|
|
|
|
|
* deems the DKG session successful.
|
|
|
|
|
*
|
|
|
|
|
* Returns: SECP256K1_CHILLDKG_OK on success, or
|
|
|
|
|
* SECP256K1_CHILLDKG_FAULTY_PARTICIPANT (with fault_index set to
|
|
|
|
|
* the signer) if a CertEq signature is invalid. On failure, all
|
|
|
|
|
* outputs are set to zero.
|
|
|
|
|
* Args: ctx: pointer to a context object
|
|
|
|
|
* Out: cmsg2: pointer to a 64*n byte array to store the
|
|
|
|
|
* certificate, to be broadcast to all participants
|
|
|
|
|
* thresh_pk33: pointer to a 33-byte array to store the threshold
|
|
|
|
|
* public key (compressed serialization)
|
|
|
|
|
* pubshares33: pointer to an array of n 33-byte elements to store
|
|
|
|
|
* the public shares of all participants
|
|
|
|
|
* recovery: pointer to a 4 + 33*t + 162*n byte array (see
|
|
|
|
|
* secp256k1_chilldkg_recovery_data_len) to store the
|
|
|
|
|
* recovery data
|
|
|
|
|
* fault_index: pointer to a uint32 (see above)
|
|
|
|
|
* In: state: pointer to the coordinator_state object output by
|
|
|
|
|
* secp256k1_chilldkg_coordinator_step1
|
|
|
|
|
* pmsgs2: array of n pointers to the participants' second
|
|
|
|
|
* messages (64 bytes each)
|
|
|
|
|
*/
|
|
|
|
|
SECP256K1_API SECP256K1_WARN_UNUSED_RESULT secp256k1_chilldkg_fault secp256k1_chilldkg_coordinator_finalize(
|
|
|
|
|
const secp256k1_context *ctx,
|
|
|
|
|
unsigned char *cmsg2,
|
|
|
|
|
unsigned char *thresh_pk33,
|
|
|
|
|
unsigned char *pubshares33,
|
|
|
|
|
unsigned char *recovery,
|
|
|
|
|
uint32_t *fault_index,
|
|
|
|
|
const secp256k1_chilldkg_coordinator_state *state,
|
|
|
|
|
const unsigned char *const *pmsgs2
|
|
|
|
|
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4) SECP256K1_ARG_NONNULL(5) SECP256K1_ARG_NONNULL(6) SECP256K1_ARG_NONNULL(7) SECP256K1_ARG_NONNULL(8);
|
|
|
|
|
|
chilldkg: Phase 5 - recovery, recovery acks, investigation
Complete the ChillDKG protocol surface with the recovery and blame-
attribution procedures (bip-frost-dkg v0.3.0-dev, reference commit
a91896883f85b159415ecf298d5e844879af112d).
Recovery:
- secp256k1_chilldkg_participant_recover / _coordinator_recover: parse
the self-delimiting recovery layout u32be(t) || sum_coms(33t) ||
hostpubkeys(33n) || pubnonces(33n) || enc_secshares(32n, checked) ||
cert(64n), deriving n = (len-4-33t)/162 exactly as the reference's
deserialize_recovery_data; re-verify the certificate, recompute the
TapTweak and the receiver's ECDH/self pads from (hostseckey,
pubnonces, enc_context), recompute the tweaked secshare, and
sanity-check secshare*G == pubshares[own]. Also return hostpubkeys,
n and t so callers can re-derive session params. RecoveryDataError /
HostSeckeyError / params failures map to INVALID_INPUT (no index,
as in the reference); an invalid pubnonce during decrypt passes
through as FAULTY_PARTICIPANT_OR_COORDINATOR(i), matching the
reference leaking that exception from recover().
Recovery acks:
- secp256k1_chilldkg_recovery_ack_sign / _acks_verify: BIP-340
(standard BIP0340 tags) over pad33("BIP DKG/recovery acknowledgment")
|| u32be(i) || recovery_data. Verification failure maps to
FAULTY_PARTICIPANT(i) (InvalidRecoveryAckError subclasses
FaultyParticipantError in the reference).
Investigation:
- secp256k1_chilldkg_coordinator_investigate: builds one 65n-byte
per-participant message (per-dealer encrypted partial secshares
(32n) + partial pubshares (33n)) per call; the reference returns all
n at once -- equivalent, the caller iterates.
- secp256k1_chilldkg_participant_investigate: the reference's
three-step blame attribution -- sum-of-pubshares check ->
FAULTY_COORDINATOR; sum-of-secshares check -> FAULTY_COORDINATOR
(covers the reference's SecshareSumError translation); per-dealer
decrypted share vs commitment -> FAULTY_PARTICIPANT_OR_COORDINATOR(i)
(or FAULTY_COORDINATOR for the own index); all-consistent ->
INVALID_INPUT (the reference's uncaught RuntimeError).
- Investigation data is transported via a new opaque, secret-bearing
secp256k1_chilldkg_participant_inv_data object (4205 bytes,
magic-validated save/load, secret-cleared) filled by
participant_step2 on the UNKNOWN_FAULT paths. This amends the
Phase 3 participant_step2 signature with a nullable inv_data
out-param -- required because recomputing inside
participant_investigate would duplicate step2's decrypt/verify
logic.
- New length helper secp256k1_chilldkg_investigation_msg_len (65n).
tests_impl.h: chilldkg_recovery_test (recover roundtrips byte-exact
vs the session outputs and reference vectors, tampered/truncated/
over-long recovery data, unknown/invalid hostseckey, ack sign
byte-exact + verify with wrong-index and tampered-ack blame,
params/recovery mismatch rejects, misuse) and
chilldkg_investigate_test (two end-to-end public-API scenarios
generated from the reference: dealer corrupting a participant's
encrypted share, and coordinator tampering with an enc_secshare;
cmsg1/cinv/inv_data byte-exact, blame codes and indices matching the
reference's exception type and index; malformed cinv ->
FAULTY_COORDINATOR; malformed pmsg1 -> FAULTY_PARTICIPANT(j); misuse).
The all-consistent investigate path is not constructible without
discrete logs and matches the reference's unreachable RuntimeError.
Verified: make check 3/3 (incl. noverify); CMake ctest 369/369;
make distdir clean.
2026-08-31 06:09:03 +02:00
|
|
|
/** Length of a coordinator investigation message (cinv): 65*n bytes (n
|
|
|
|
|
* encrypted partial secret shares of 32 bytes each, followed by n partial
|
|
|
|
|
* public shares of 33 bytes each). Returns 0 if n is out of range. */
|
|
|
|
|
SECP256K1_API size_t secp256k1_chilldkg_investigation_msg_len(
|
|
|
|
|
size_t n_participants
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
/** Recover the DKG output of a participant from recovery data.
|
|
|
|
|
*
|
|
|
|
|
* This function serves two purposes: recovering from a failure of
|
|
|
|
|
* secp256k1_chilldkg_participant_finalize after obtaining the recovery data
|
|
|
|
|
* from another participant or the coordinator, and reproducing the DKG
|
|
|
|
|
* outputs on a new device (e.g., after data loss).
|
|
|
|
|
*
|
|
|
|
|
* The recovery data (see secp256k1_chilldkg_recovery_data_len) is
|
|
|
|
|
* self-delimiting: the number of participants n and the threshold t are
|
|
|
|
|
* derived from its length and contents.
|
|
|
|
|
*
|
|
|
|
|
* Returns: SECP256K1_CHILLDKG_OK on success,
|
|
|
|
|
* SECP256K1_CHILLDKG_INVALID_INPUT if the recovery data is invalid
|
|
|
|
|
* (this includes an invalid certificate) or if the host secret key
|
|
|
|
|
* is invalid or does not match any host public key in the recovery
|
|
|
|
|
* data (RecoveryDataError and HostSeckeyError in the reference
|
|
|
|
|
* implementation), or
|
|
|
|
|
* SECP256K1_CHILLDKG_FAULTY_PARTICIPANT_OR_COORDINATOR (with
|
|
|
|
|
* fault_index set) if a pubnonce in the recovery data is invalid.
|
|
|
|
|
* On failure, secshare32 and thresh_pk33 are set to zero and
|
|
|
|
|
* *n_participants_out and *threshold_out are set to 0; pubshares33
|
|
|
|
|
* and hostpubkeys33_out are set to zero whenever n could be
|
|
|
|
|
* determined.
|
|
|
|
|
* Args: ctx: pointer to a context object
|
|
|
|
|
* Out: secshare32: pointer to a 32-byte array to store the
|
|
|
|
|
* (tweaked) secret share
|
|
|
|
|
* thresh_pk33: pointer to a 33-byte array to store the
|
|
|
|
|
* threshold public key (compressed serialization)
|
|
|
|
|
* pubshares33: pointer to an array capable of holding
|
|
|
|
|
* 33*SECP256K1_CHILLDKG_MAX_PARTICIPANTS bytes
|
|
|
|
|
* to store the public shares (33 bytes each; the
|
|
|
|
|
* first n entries are filled)
|
|
|
|
|
* hostpubkeys33_out: pointer to an array capable of holding
|
|
|
|
|
* 33*SECP256K1_CHILLDKG_MAX_PARTICIPANTS bytes
|
|
|
|
|
* to store the host public keys of the recovered
|
|
|
|
|
* session (the first n entries are filled)
|
|
|
|
|
* n_participants_out: pointer to a size_t to store the number of
|
|
|
|
|
* participants n of the recovered session
|
|
|
|
|
* threshold_out: pointer to a uint32 to store the threshold t
|
|
|
|
|
* of the recovered session
|
|
|
|
|
* fault_index: pointer to a uint32 that receives the identifier
|
|
|
|
|
* of the suspected participant where applicable,
|
|
|
|
|
* and UINT32_MAX otherwise
|
|
|
|
|
* In: hostseckey32: pointer to the 32-byte host secret key
|
|
|
|
|
* recovery: pointer to the recovery data
|
|
|
|
|
* recovery_len: length of the recovery data
|
|
|
|
|
*/
|
|
|
|
|
SECP256K1_API SECP256K1_WARN_UNUSED_RESULT secp256k1_chilldkg_fault secp256k1_chilldkg_participant_recover(
|
|
|
|
|
const secp256k1_context *ctx,
|
|
|
|
|
unsigned char *secshare32,
|
|
|
|
|
unsigned char *thresh_pk33,
|
|
|
|
|
unsigned char *pubshares33,
|
|
|
|
|
unsigned char *hostpubkeys33_out,
|
|
|
|
|
size_t *n_participants_out,
|
|
|
|
|
uint32_t *threshold_out,
|
|
|
|
|
uint32_t *fault_index,
|
|
|
|
|
const unsigned char *hostseckey32,
|
|
|
|
|
const unsigned char *recovery,
|
|
|
|
|
size_t recovery_len
|
|
|
|
|
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4) SECP256K1_ARG_NONNULL(5) SECP256K1_ARG_NONNULL(6) SECP256K1_ARG_NONNULL(7) SECP256K1_ARG_NONNULL(8) SECP256K1_ARG_NONNULL(9) SECP256K1_ARG_NONNULL(10);
|
|
|
|
|
|
|
|
|
|
/** Recover the DKG output of the coordinator from recovery data.
|
|
|
|
|
*
|
|
|
|
|
* Like secp256k1_chilldkg_participant_recover, but for the coordinator, who
|
|
|
|
|
* has no secret share.
|
|
|
|
|
*
|
|
|
|
|
* Returns: SECP256K1_CHILLDKG_OK on success,
|
|
|
|
|
* SECP256K1_CHILLDKG_INVALID_INPUT if the recovery data is invalid.
|
|
|
|
|
* On failure, thresh_pk33 is set to zero and *n_participants_out
|
|
|
|
|
* and *threshold_out are set to 0; pubshares33 and
|
|
|
|
|
* hostpubkeys33_out are set to zero whenever n could be determined.
|
|
|
|
|
* Args: ctx: pointer to a context object
|
|
|
|
|
* Out: thresh_pk33: pointer to a 33-byte array to store the
|
|
|
|
|
* threshold public key (compressed serialization)
|
|
|
|
|
* pubshares33: pointer to an array capable of holding
|
|
|
|
|
* 33*SECP256K1_CHILLDKG_MAX_PARTICIPANTS bytes
|
|
|
|
|
* to store the public shares (the first n
|
|
|
|
|
* entries are filled)
|
|
|
|
|
* hostpubkeys33_out: pointer to an array capable of holding
|
|
|
|
|
* 33*SECP256K1_CHILLDKG_MAX_PARTICIPANTS bytes
|
|
|
|
|
* to store the host public keys of the recovered
|
|
|
|
|
* session (the first n entries are filled)
|
|
|
|
|
* n_participants_out: pointer to a size_t to store n
|
|
|
|
|
* threshold_out: pointer to a uint32 to store t
|
|
|
|
|
* In: recovery: pointer to the recovery data
|
|
|
|
|
* recovery_len: length of the recovery data
|
|
|
|
|
*/
|
|
|
|
|
SECP256K1_API SECP256K1_WARN_UNUSED_RESULT secp256k1_chilldkg_fault secp256k1_chilldkg_coordinator_recover(
|
|
|
|
|
const secp256k1_context *ctx,
|
|
|
|
|
unsigned char *thresh_pk33,
|
|
|
|
|
unsigned char *pubshares33,
|
|
|
|
|
unsigned char *hostpubkeys33_out,
|
|
|
|
|
size_t *n_participants_out,
|
|
|
|
|
uint32_t *threshold_out,
|
|
|
|
|
const unsigned char *recovery,
|
|
|
|
|
size_t recovery_len
|
|
|
|
|
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4) SECP256K1_ARG_NONNULL(5) SECP256K1_ARG_NONNULL(6) SECP256K1_ARG_NONNULL(7);
|
|
|
|
|
|
|
|
|
|
/** Sign recovery data to create a recovery acknowledgment.
|
|
|
|
|
*
|
|
|
|
|
* The acknowledgment signature (64 bytes) is a BIP 340 signature over
|
|
|
|
|
* pad33("BIP DKG/recovery acknowledgment") || u32be(participant_id) ||
|
|
|
|
|
* recovery_data under the participant's host key. Acks can be collected in
|
|
|
|
|
* an optional acknowledgment round to confirm that all participants have
|
|
|
|
|
* received the recovery data.
|
|
|
|
|
*
|
|
|
|
|
* Returns: 1 on success, 0 on invalid input (invalid host secret key, host
|
|
|
|
|
* secret key not matching any host public key, invalid session
|
|
|
|
|
* parameters, or recovery data that is invalid or does not match
|
|
|
|
|
* the session parameters). On failure, sig64 is set to zero.
|
|
|
|
|
* Args: ctx: pointer to a context object
|
|
|
|
|
* Out: sig64: pointer to a 64-byte array to store the
|
|
|
|
|
* acknowledgment signature
|
|
|
|
|
* In: hostseckey32: pointer to the 32-byte host secret key
|
|
|
|
|
* hostpubkeys33: pointer to an array of n host public keys (33 bytes
|
|
|
|
|
* each)
|
|
|
|
|
* n_participants: total number of participants n
|
|
|
|
|
* threshold: threshold t
|
|
|
|
|
* recovery: pointer to the recovery data
|
|
|
|
|
* recovery_len: length of the recovery data
|
|
|
|
|
* aux_rand32: pointer to 32 bytes of auxiliary randomness (see
|
|
|
|
|
* BIP 340)
|
|
|
|
|
*/
|
|
|
|
|
SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_chilldkg_recovery_ack_sign(
|
|
|
|
|
const secp256k1_context *ctx,
|
|
|
|
|
unsigned char *sig64,
|
|
|
|
|
const unsigned char *hostseckey32,
|
|
|
|
|
const unsigned char *hostpubkeys33,
|
|
|
|
|
size_t n_participants,
|
|
|
|
|
uint32_t threshold,
|
|
|
|
|
const unsigned char *recovery,
|
|
|
|
|
size_t recovery_len,
|
|
|
|
|
const unsigned char *aux_rand32
|
|
|
|
|
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4) SECP256K1_ARG_NONNULL(7) SECP256K1_ARG_NONNULL(9);
|
|
|
|
|
|
|
|
|
|
/** Verify recovery acknowledgment signatures from all participants.
|
|
|
|
|
*
|
|
|
|
|
* Note that a failure of this function does NOT mean the DKG failed
|
|
|
|
|
* (reaching this point implies the DKG itself was successful). It only means
|
|
|
|
|
* it cannot be confirmed that all participants have a copy of the recovery
|
|
|
|
|
* data.
|
|
|
|
|
*
|
|
|
|
|
* Returns: SECP256K1_CHILLDKG_OK if all n acknowledgment signatures are
|
|
|
|
|
* valid, SECP256K1_CHILLDKG_INVALID_INPUT on invalid session
|
|
|
|
|
* parameters or recovery data, or SECP256K1_CHILLDKG_FAULTY_PARTICIPANT
|
|
|
|
|
* (with fault_index set to the signer) if an acknowledgment
|
|
|
|
|
* signature is invalid (InvalidRecoveryAckError in the reference
|
|
|
|
|
* implementation).
|
|
|
|
|
* Args: ctx: pointer to a context object
|
|
|
|
|
* Out: fault_index: pointer to a uint32 that receives the identifier of
|
|
|
|
|
* the participant whose signature is invalid, where
|
|
|
|
|
* applicable, and UINT32_MAX otherwise
|
|
|
|
|
* In: hostpubkeys33: pointer to an array of n host public keys (33 bytes
|
|
|
|
|
* each)
|
|
|
|
|
* n_participants: total number of participants n
|
|
|
|
|
* threshold: threshold t
|
|
|
|
|
* recovery: pointer to the recovery data
|
|
|
|
|
* recovery_len: length of the recovery data
|
|
|
|
|
* ack_sigs64: array of n pointers to the acknowledgment
|
|
|
|
|
* signatures (64 bytes each), in the same order as
|
|
|
|
|
* hostpubkeys33
|
|
|
|
|
*/
|
|
|
|
|
SECP256K1_API SECP256K1_WARN_UNUSED_RESULT secp256k1_chilldkg_fault secp256k1_chilldkg_recovery_acks_verify(
|
|
|
|
|
const secp256k1_context *ctx,
|
|
|
|
|
uint32_t *fault_index,
|
|
|
|
|
const unsigned char *hostpubkeys33,
|
|
|
|
|
size_t n_participants,
|
|
|
|
|
uint32_t threshold,
|
|
|
|
|
const unsigned char *recovery,
|
|
|
|
|
size_t recovery_len,
|
|
|
|
|
const unsigned char *const *ack_sigs64
|
|
|
|
|
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(6) SECP256K1_ARG_NONNULL(8);
|
|
|
|
|
|
|
|
|
|
/** Generate the investigation message for a single participant.
|
|
|
|
|
*
|
|
|
|
|
* The investigation message (65*n bytes, see
|
|
|
|
|
* secp256k1_chilldkg_investigation_msg_len) allows the given participant to
|
|
|
|
|
* investigate who is to blame for a failed ChillDKG session (see
|
|
|
|
|
* secp256k1_chilldkg_participant_investigate). The message contains no
|
|
|
|
|
* confidential information and can be safely broadcast. (Unlike the
|
|
|
|
|
* reference implementation, which computes the investigation messages for
|
|
|
|
|
* all participants at once, this function computes one message per call.)
|
|
|
|
|
*
|
|
|
|
|
* Returns: SECP256K1_CHILLDKG_OK on success,
|
|
|
|
|
* SECP256K1_CHILLDKG_INVALID_INPUT on invalid session parameters or
|
|
|
|
|
* participant_id >= n, or SECP256K1_CHILLDKG_FAULTY_PARTICIPANT
|
|
|
|
|
* (with fault_index set to the sender) if a participant message is
|
|
|
|
|
* malformed. On failure, cinv is set to zero.
|
|
|
|
|
* Args: ctx: pointer to a context object
|
|
|
|
|
* Out: cinv: pointer to a 65*n byte array to store the
|
|
|
|
|
* investigation message for the given participant
|
|
|
|
|
* fault_index: pointer to a uint32 (see above)
|
|
|
|
|
* In: pmsgs1: array of n pointers to the participants' first
|
|
|
|
|
* messages (33*t + 32*n + 97 bytes each)
|
|
|
|
|
* hostpubkeys33: pointer to an array of n host public keys (33 bytes
|
|
|
|
|
* each)
|
|
|
|
|
* n_participants: total number of participants n
|
|
|
|
|
* threshold: threshold t
|
|
|
|
|
* participant_id: the participant the investigation message is for
|
|
|
|
|
*/
|
|
|
|
|
SECP256K1_API SECP256K1_WARN_UNUSED_RESULT secp256k1_chilldkg_fault secp256k1_chilldkg_coordinator_investigate(
|
|
|
|
|
const secp256k1_context *ctx,
|
|
|
|
|
unsigned char *cinv,
|
|
|
|
|
uint32_t *fault_index,
|
|
|
|
|
const unsigned char *const *pmsgs1,
|
|
|
|
|
const unsigned char *hostpubkeys33,
|
|
|
|
|
size_t n_participants,
|
|
|
|
|
uint32_t threshold,
|
|
|
|
|
uint32_t participant_id
|
|
|
|
|
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4) SECP256K1_ARG_NONNULL(5);
|
|
|
|
|
|
|
|
|
|
/** Investigate who is to blame for a failed ChillDKG session.
|
|
|
|
|
*
|
|
|
|
|
* This function can be called when secp256k1_chilldkg_participant_step2
|
|
|
|
|
* returned SECP256K1_CHILLDKG_UNKNOWN_FAULTY_PARTICIPANT_OR_COORDINATOR. It
|
|
|
|
|
* narrows down the suspected faulty parties by analyzing the investigation
|
|
|
|
|
* message provided by the coordinator.
|
|
|
|
|
*
|
|
|
|
|
* Returns: SECP256K1_CHILLDKG_FAULTY_COORDINATOR if the coordinator is
|
|
|
|
|
* faulty, SECP256K1_CHILLDKG_FAULTY_PARTICIPANT_OR_COORDINATOR
|
|
|
|
|
* (with fault_index set to the suspected participant) if another
|
|
|
|
|
* participant or the coordinator is faulty, or
|
|
|
|
|
* SECP256K1_CHILLDKG_INVALID_INPUT if all inputs are consistent
|
|
|
|
|
* (i.e., the function was called even though no fault occurred).
|
|
|
|
|
* Args: ctx: pointer to a context object
|
|
|
|
|
* Out: fault_index: pointer to a uint32 (see above)
|
|
|
|
|
* In: inv_data: pointer to the participant_inv_data object output by
|
|
|
|
|
* secp256k1_chilldkg_participant_step2
|
|
|
|
|
* cinv: pointer to the coordinator's investigation message
|
|
|
|
|
* for this participant (65*n bytes, see
|
|
|
|
|
* secp256k1_chilldkg_investigation_msg_len)
|
|
|
|
|
*/
|
|
|
|
|
SECP256K1_API SECP256K1_WARN_UNUSED_RESULT secp256k1_chilldkg_fault secp256k1_chilldkg_participant_investigate(
|
|
|
|
|
const secp256k1_context *ctx,
|
|
|
|
|
uint32_t *fault_index,
|
|
|
|
|
const secp256k1_chilldkg_participant_inv_data *inv_data,
|
|
|
|
|
const unsigned char *cinv
|
|
|
|
|
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4);
|
|
|
|
|
|
chilldkg: Phase 0 - module scaffolding and build wiring
Add an empty, experimental `chilldkg` module as the foundation for a
ChillDKG implementation (distributed key generation for FROST) per the
bip-frost-dkg BIP draft (v0.3.0-dev):
https://github.com/BlockstreamResearch/bip-frost-dkg
The module lives in src/modules/chilldkg/ (separate from the frost
module, per the implementation plan in .idea/docs/
chilldkg-implementation-plan.md: FROST signing (BIP 445) and ChillDKG
are separate BIPs with separate reference repos, test vectors and
review cycles; the dependency between them is one-way bytes).
New files:
- include/secp256k1_chilldkg.h: public header skeleton with the same
"EXTREMELY DANGEROUS / work in progress" warning style as
secp256k1_frost.h, plus a note that the BIP is a draft and tagged
hashes/wire formats may change. No API yet (Phase 3+).
- src/modules/chilldkg/main_impl.h: implementation skeleton including
the public header.
- src/modules/chilldkg/tests_impl.h: trivial scaffolding unit test
(chilldkg_scaffolding_test) registered via the tests_chilldkg[]
CASE1 array used by this repo's unit-test framework.
- src/modules/chilldkg/Makefile.am.include: autotools file list,
mirroring the frost module's.
- src/modules/chilldkg/chilldkg.md: module doc stub (purpose, draft
status, dependency on the schnorrsig and ecdh modules).
Build wiring (mirrors the frost module exactly):
- configure.ac: --enable-module-chilldkg (default no, experimental
gate), dependency errors when schnorrsig or ecdh are explicitly
disabled, AM_CONDITIONAL(ENABLE_MODULE_CHILLDKG), summary line.
- Makefile.am: include src/modules/chilldkg/Makefile.am.include under
ENABLE_MODULE_CHILLDKG.
- src/secp256k1.c: guarded include of modules/chilldkg/main_impl.h
after the frost module.
- src/tests.c: guarded include of tests_impl.h and
MAKE_TEST_MODULE(chilldkg) registration.
- CMakeLists.txt: SECP256K1_ENABLE_MODULE_CHILLDKG option (OFF) +
summary line.
- src/CMakeLists.txt: dependency checks on
SECP256K1_ENABLE_MODULE_SCHNORRSIG and SECP256K1_ENABLE_MODULE_ECDH,
ENABLE_MODULE_CHILLDKG=1 compile definition, public header export.
Verified:
- ./autogen.sh && ./configure --enable-experimental
--enable-module-chilldkg --enable-module-schnorrsig
--enable-module-ecdh && make check: PASS 3/3 (tests, noverify_tests,
exhaustive_tests).
- configure fails with a clear error when schnorrsig or ecdh are
disabled, or when experimental is not enabled.
- CMake build with SECP256K1_ENABLE_MODULE_CHILLDKG=ON: ctest 345/345
passed; dependency errors fire correctly when schnorrsig/ecdh OFF.
2026-08-31 01:37:22 +02:00
|
|
|
#ifdef __cplusplus
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
#endif
|