Files
secp256k1-zkp/src/modules/chilldkg/util.h

106 lines
6.8 KiB
C
Raw Normal View History

chilldkg: Phase 1 - internal primitives (util, vss) Add the byte-exact internal primitives for the ChillDKG module, mirroring the Python reference implementation of the bip-frost-dkg draft (v0.3.0-dev), pinned to upstream commit a91896883f85b159415ecf298d5e844879af112d. util.h / util_impl.h (mirrors chilldkg_ref/util.py): - Point (de)serialization with explicit point-at-infinity support: 33 zero bytes <-> infinity, otherwise SEC compressed. Checked parse rejects invalid encodings and out-of-range x coordinates (point_save/point_load, xonly_save/xonly_load). - Internal parameterized-tag BIP-340 Schnorr sign/verify (chilldkg_schnorrsig_sign/_verify): tag prefix selects the <prefix>/aux, /nonce, /challenge subtags ("BIP DKG/pop message" for proofs of possession, "BIP0340" for CertEq signatures and recovery acks), arbitrary-length messages, pad33 zero-padding helper. The public schnorrsig API hardcodes BIP0340/32-byte messages, so the algorithm is replicated from secp256k1_schnorrsig_sign_internal with a custom tag; cross-checked against secp256k1_schnorrsig_sign32. - Tagged hashes via secp256k1_sha256_initialize_tagged: "BIP DKG/params_hash", "BIP DKG/encpedpop seed", "BIP DKG/simplpedpop aux", "BIP DKG/encpedpop secnonce", "BIP DKG/encpedpop ecdh", "BIP DKG/encaps_multi self_pad", "BIP DKG/vss coeffs", and BIP-341 "TapTweak" (32-byte x-only input). - params_hash = TH("BIP DKG/params_hash", u32be(t) || hostpubkeys) (note: plan had the operand order reversed; the reference hashes t first). - ECDH pads: reuses the ecdh module's SHA256-of-compressed-shared- point hash, then TH("BIP DKG/encpedpop ecdh", ecdh || sender_pubnonce || receiver_hostpubkey || context) with a sending flag fixing the sender|receiver order; self_pad for the own index. Pads are parsed wrapping (mod-n reduction); wire scalars, VSS coefficients and the TapTweak are parsed checked. vss.h / vss_impl.h (mirrors chilldkg_ref/vss.py): - vss_gen_coeffs: per-coefficient TH("BIP DKG/vss coeffs", seed || u32be(j)), checked parse with bitwise error accumulation. - vss_poly_eval (Horner) and vss_secshare_for with the x = id+1 convention (safe at UINT32_MAX). - vss_commit (constant-time ecmult_gen, zero coefficient -> infinity), vss_pubshare (powers-of-x over commitments, skips infinity), vss_commitment_add, vss_verify_secshare. - vss_invalid_taproot_commit: TapTweak applied to the x-only constant term so the Taproot script path is unspendable; returns tweak and pubtweak. tests_impl.h: 7 vector tests (tagged hashes, params_hash, point serialization incl. infinity roundtrip and parity prefixes, checked vs wrapping scalar parse at the group order boundary, custom-tag schnorrsig incl. wrong-tag/key/msg rejection, ECDH pad sender/receiver symmetry, VSS coeff derivation/Horner/commitment/pubshare/tweak) with expected values generated once from the Python reference (committed into the test file, reference commit recorded). Verified: make check 3/3 suites pass; CMake ctest all pass; ./tests --target=chilldkg runs all 7 new tests green in both verify and noverify builds.
2026-08-31 04:05:15 +02:00
/***********************************************************************
* Distributed under the MIT software license, see the accompanying *
* file COPYING or https://www.opensource.org/licenses/mit-license.php.*
***********************************************************************/
#ifndef SECP256K1_MODULE_CHILLDKG_UTIL_H
#define SECP256K1_MODULE_CHILLDKG_UTIL_H
#include "../../../include/secp256k1.h"
chilldkg: Phase 3 - public participant API and CertEq Add the public participant-facing ChillDKG API to include/secp256k1_chilldkg.h and the CertEq sub-protocol, completing the participant side of the protocol (bip-frost-dkg v0.3.0-dev, reference pinned at a91896883f85b159415ecf298d5e844879af112d). New module files: - certeq.h / certeq_impl.h: CertEq sub-protocol. Participants sign pad33("BIP DKG/certeq message") || u32be(i) || eq_input with plain BIP0340-tagged Schnorr signatures under their host key (certeq_participant_step); verification is per-index against the x-only hostpubkeys[i][1:33] exactly as the reference (certeq_verify). The coordinator side reuses certeq_verify in Phase 4. Public API (all no-malloc, caller-allocated buffers, outputs zeroed on failure, secret paths cleared): - secp256k1_chilldkg_hostpubkey_gen: plain compressed host pubkey generation; rejects zero / >= group order seckeys. - secp256k1_chilldkg_params_hash: validates session params (participant and threshold ranges, strictly compressed non-infinity pubkeys, no duplicates) and computes TH("BIP DKG/params_hash", u32be(t) || hostpubkeys). - Message-length helpers so callers can size buffers: participant_msg1_len (33t+32n+97), coordinator_msg1_len (162n+33(t-1)), participant_msg2_len (64), coordinator_msg2_len (64n), recovery_data_len (4+33t+162n). - secp256k1_chilldkg_participant_step1: full EncPedPop step1 with seed=deckey=hostseckey; rejects zero randomness and hostseckeys not matching the claimed hostpubkey (input errors, not protocol faults). - secp256k1_chilldkg_participant_step2: parses and verifies cmsg1 via the Phase 2 encpedpop/simplpedpop participant path, computes the tweaked secshare/pubshares/threshold pubkey, appends enc_secshares to eq_input (matching the reference for recovery consistency), and emits the 64-byte CertEq signature. - secp256k1_chilldkg_participant_finalize: re-verifies all n CertEq signatures in the certificate, then outputs the 32-byte secshare, 33-byte threshold pubkey, n pubshares and the self-delimiting recovery data (eq_input || cert). Blame reporting without exceptions: public enum secp256k1_chilldkg_fault (OK / FAULTY_COORDINATOR / FAULTY_PARTICIPANT / FAULTY_PARTICIPANT_OR_COORDINATOR / UNKNOWN_FAULTY_PARTICIPANT_OR_COORDINATOR / INVALID_INPUT) plus an out fault_index, mapping the reference's exception taxonomy: - hostseckey invalid/mismatch -> INVALID_INPUT (HostSeckeyError), - cmsg1 scalar overflow/parse -> FAULTY_COORDINATOR (MsgParseError), - pubnonce/commitment/PoP faults -> FAULTY_PARTICIPANT_OR_COORDINATOR(i), - share-vs-pubshare mismatch -> UNKNOWN with fault_index = UINT32_MAX, - certificate signature failure -> FAULTY_COORDINATOR (documented deviation: fault_index carries the failing signature index as diagnostic info; the reference discards it). Enum-returning functions use a local CHILLDKG_ARG_CHECK that fires the illegal-argument callback and returns INVALID_INPUT (ARG_CHECK would return 0 = OK). Opaque state objects with magic-validated save/load (frost idiom): participant_state1 (4306 bytes, no secrets) and participant_state2 (21073 bytes, contains the secshare; documented keep-secret/no-copy). Fixed-size at SECP256K1_CHILLDKG_MAX_PARTICIPANTS = 128. Also fixes a noverify-build bug: state1_load ran point_load inside VERIFY_CHECK, which compiles out in noverify builds and left the commitment uninitialized; now called unconditionally. tests_impl.h: participant_api_test with full-session reference vectors (n=3, t=2; coordinator aggregation simulated through the internal Phase 2 coordinator step and verified byte-identical to the reference's coordinator_step1): msglen helpers, hostpubkey_gen and params_hash vectors incl. duplicate/invalid/infinity rejection, byte-exact pmsg1/cmsg1/CertEq sigs/secshare/thresh_pk/pubshares/ recovery, blame cases (tampered enc_secshare -> UNKNOWN, invalid pubnonce -> FAULTY_PARTICIPANT_OR_COORDINATOR(1), overflowing enc_secshare -> FAULTY_COORDINATOR, corrupted cert sig -> FAULTY_COORDINATOR with fault_index and zeroed outputs), NULL-arg misuse and bad-magic state rejection. Verified: make check 3/3 (incl. noverify); CMake ctest 363/363; make distdir includes all new files.
2026-08-31 05:22:53 +02:00
#include "../../../include/secp256k1_chilldkg.h"
chilldkg: Phase 1 - internal primitives (util, vss) Add the byte-exact internal primitives for the ChillDKG module, mirroring the Python reference implementation of the bip-frost-dkg draft (v0.3.0-dev), pinned to upstream commit a91896883f85b159415ecf298d5e844879af112d. util.h / util_impl.h (mirrors chilldkg_ref/util.py): - Point (de)serialization with explicit point-at-infinity support: 33 zero bytes <-> infinity, otherwise SEC compressed. Checked parse rejects invalid encodings and out-of-range x coordinates (point_save/point_load, xonly_save/xonly_load). - Internal parameterized-tag BIP-340 Schnorr sign/verify (chilldkg_schnorrsig_sign/_verify): tag prefix selects the <prefix>/aux, /nonce, /challenge subtags ("BIP DKG/pop message" for proofs of possession, "BIP0340" for CertEq signatures and recovery acks), arbitrary-length messages, pad33 zero-padding helper. The public schnorrsig API hardcodes BIP0340/32-byte messages, so the algorithm is replicated from secp256k1_schnorrsig_sign_internal with a custom tag; cross-checked against secp256k1_schnorrsig_sign32. - Tagged hashes via secp256k1_sha256_initialize_tagged: "BIP DKG/params_hash", "BIP DKG/encpedpop seed", "BIP DKG/simplpedpop aux", "BIP DKG/encpedpop secnonce", "BIP DKG/encpedpop ecdh", "BIP DKG/encaps_multi self_pad", "BIP DKG/vss coeffs", and BIP-341 "TapTweak" (32-byte x-only input). - params_hash = TH("BIP DKG/params_hash", u32be(t) || hostpubkeys) (note: plan had the operand order reversed; the reference hashes t first). - ECDH pads: reuses the ecdh module's SHA256-of-compressed-shared- point hash, then TH("BIP DKG/encpedpop ecdh", ecdh || sender_pubnonce || receiver_hostpubkey || context) with a sending flag fixing the sender|receiver order; self_pad for the own index. Pads are parsed wrapping (mod-n reduction); wire scalars, VSS coefficients and the TapTweak are parsed checked. vss.h / vss_impl.h (mirrors chilldkg_ref/vss.py): - vss_gen_coeffs: per-coefficient TH("BIP DKG/vss coeffs", seed || u32be(j)), checked parse with bitwise error accumulation. - vss_poly_eval (Horner) and vss_secshare_for with the x = id+1 convention (safe at UINT32_MAX). - vss_commit (constant-time ecmult_gen, zero coefficient -> infinity), vss_pubshare (powers-of-x over commitments, skips infinity), vss_commitment_add, vss_verify_secshare. - vss_invalid_taproot_commit: TapTweak applied to the x-only constant term so the Taproot script path is unspendable; returns tweak and pubtweak. tests_impl.h: 7 vector tests (tagged hashes, params_hash, point serialization incl. infinity roundtrip and parity prefixes, checked vs wrapping scalar parse at the group order boundary, custom-tag schnorrsig incl. wrong-tag/key/msg rejection, ECDH pad sender/receiver symmetry, VSS coeff derivation/Horner/commitment/pubshare/tweak) with expected values generated once from the Python reference (committed into the test file, reference commit recorded). Verified: make check 3/3 suites pass; CMake ctest all pass; ./tests --target=chilldkg runs all 7 new tests green in both verify and noverify builds.
2026-08-31 04:05:15 +02:00
#include "../../group.h"
#include "../../hash.h"
#include "../../scalar.h"
chilldkg: Phase 3 - public participant API and CertEq Add the public participant-facing ChillDKG API to include/secp256k1_chilldkg.h and the CertEq sub-protocol, completing the participant side of the protocol (bip-frost-dkg v0.3.0-dev, reference pinned at a91896883f85b159415ecf298d5e844879af112d). New module files: - certeq.h / certeq_impl.h: CertEq sub-protocol. Participants sign pad33("BIP DKG/certeq message") || u32be(i) || eq_input with plain BIP0340-tagged Schnorr signatures under their host key (certeq_participant_step); verification is per-index against the x-only hostpubkeys[i][1:33] exactly as the reference (certeq_verify). The coordinator side reuses certeq_verify in Phase 4. Public API (all no-malloc, caller-allocated buffers, outputs zeroed on failure, secret paths cleared): - secp256k1_chilldkg_hostpubkey_gen: plain compressed host pubkey generation; rejects zero / >= group order seckeys. - secp256k1_chilldkg_params_hash: validates session params (participant and threshold ranges, strictly compressed non-infinity pubkeys, no duplicates) and computes TH("BIP DKG/params_hash", u32be(t) || hostpubkeys). - Message-length helpers so callers can size buffers: participant_msg1_len (33t+32n+97), coordinator_msg1_len (162n+33(t-1)), participant_msg2_len (64), coordinator_msg2_len (64n), recovery_data_len (4+33t+162n). - secp256k1_chilldkg_participant_step1: full EncPedPop step1 with seed=deckey=hostseckey; rejects zero randomness and hostseckeys not matching the claimed hostpubkey (input errors, not protocol faults). - secp256k1_chilldkg_participant_step2: parses and verifies cmsg1 via the Phase 2 encpedpop/simplpedpop participant path, computes the tweaked secshare/pubshares/threshold pubkey, appends enc_secshares to eq_input (matching the reference for recovery consistency), and emits the 64-byte CertEq signature. - secp256k1_chilldkg_participant_finalize: re-verifies all n CertEq signatures in the certificate, then outputs the 32-byte secshare, 33-byte threshold pubkey, n pubshares and the self-delimiting recovery data (eq_input || cert). Blame reporting without exceptions: public enum secp256k1_chilldkg_fault (OK / FAULTY_COORDINATOR / FAULTY_PARTICIPANT / FAULTY_PARTICIPANT_OR_COORDINATOR / UNKNOWN_FAULTY_PARTICIPANT_OR_COORDINATOR / INVALID_INPUT) plus an out fault_index, mapping the reference's exception taxonomy: - hostseckey invalid/mismatch -> INVALID_INPUT (HostSeckeyError), - cmsg1 scalar overflow/parse -> FAULTY_COORDINATOR (MsgParseError), - pubnonce/commitment/PoP faults -> FAULTY_PARTICIPANT_OR_COORDINATOR(i), - share-vs-pubshare mismatch -> UNKNOWN with fault_index = UINT32_MAX, - certificate signature failure -> FAULTY_COORDINATOR (documented deviation: fault_index carries the failing signature index as diagnostic info; the reference discards it). Enum-returning functions use a local CHILLDKG_ARG_CHECK that fires the illegal-argument callback and returns INVALID_INPUT (ARG_CHECK would return 0 = OK). Opaque state objects with magic-validated save/load (frost idiom): participant_state1 (4306 bytes, no secrets) and participant_state2 (21073 bytes, contains the secshare; documented keep-secret/no-copy). Fixed-size at SECP256K1_CHILLDKG_MAX_PARTICIPANTS = 128. Also fixes a noverify-build bug: state1_load ran point_load inside VERIFY_CHECK, which compiles out in noverify builds and left the commitment uninitialized; now called unconditionally. tests_impl.h: participant_api_test with full-session reference vectors (n=3, t=2; coordinator aggregation simulated through the internal Phase 2 coordinator step and verified byte-identical to the reference's coordinator_step1): msglen helpers, hostpubkey_gen and params_hash vectors incl. duplicate/invalid/infinity rejection, byte-exact pmsg1/cmsg1/CertEq sigs/secshare/thresh_pk/pubshares/ recovery, blame cases (tampered enc_secshare -> UNKNOWN, invalid pubnonce -> FAULTY_PARTICIPANT_OR_COORDINATOR(1), overflowing enc_secshare -> FAULTY_COORDINATOR, corrupted cert sig -> FAULTY_COORDINATOR with fault_index and zeroed outputs), NULL-arg misuse and bad-magic state rejection. Verified: make check 3/3 (incl. noverify); CMake ctest 363/363; make distdir includes all new files.
2026-08-31 05:22:53 +02:00
/* The public header defines SECP256K1_CHILLDKG_MAX_PARTICIPANTS and the fault
* enum secp256k1_chilldkg_fault (SECP256K1_CHILLDKG_OK, ...), which the
* internal SimplPedPop/EncPedPop functions use directly as their return
* codes. */
chilldkg: Phase 2 - SimplPedPop and EncPedPop layers Port the two sub-protocol layers of the ChillDKG reference implementation (bip-frost-dkg v0.3.0-dev, upstream commit a91896883f85b159415ecf298d5e844879af112d) to C, keeping the same function decomposition as simplpedpop.py / encpedpop.py so the code stays diffable against the reference. simplpedpop.h / simplpedpop_impl.h (mirrors simplpedpop.py): - simplpedpop_participant_step1: VSS coefficient generation and commitment, per-participant shares, proof of possession. The PoP is a BIP-340 signature with custom tag prefix "BIP DKG/pop message" over u32be(participant_id), signed with the constant coefficient f(0), using TH("BIP DKG/simplpedpop aux", simpl_seed) as aux_rand. - simplpedpop_coordinator_step / assemble_sum_coms: echo per-dealer constant-term commitments, sum non-constant-term commitments, collect PoPs (not verified by the coordinator, as in the reference). - simplpedpop_participant_step2: own-commitment echo check, per-dealer infinity rejection and PoP verification against the x-only coms_to_secrets[i], TapTweak applied before share verification (secshare vs pubshare check on tweaked values), eq_input = u32be(t) || sum_coms committing to the UNTWEAKED summed commitment. encpedpop.h / encpedpop_impl.h (mirrors encpedpop.py): - simpl_seed / aux / secnonce derivations from (hostseckey, random, enc_context) via "BIP DKG/encpedpop seed", "BIP DKG/simplpedpop aux" and "BIP DKG/encpedpop secnonce"; pubnonce = pubkey_gen_plain(secnonce). - encaps_multi / encrypt_multi: per-recipient pad context u32be(i) || enc_context, self-pad at the own index (no ECDH), libsecp256k1-style ECDH pad otherwise with sender-first ordering; encryption is additive mod n so the coordinator can sum encrypted shares per recipient. - decaps_multi / decrypt_sum: receiver-side pads, invalid or infinity pubnonce maps to FAULTY_PARTICIPANT_OR_COORDINATOR(sender). - coordinator_step: checked scalar parse of encrypted shares (overflow blames the sender), per-recipient summation. - participant_step2: pubnonce echo check (mismatch -> FAULTY_COORDINATOR), decrypt, delegate to simplpedpop step2, eq_input extended with enckeys || pubnonces. util.h: add SECP256K1_CHILLDKG_MAX_PARTICIPANTS (128, matching frost) and the internal fault enum mirroring the reference's exception taxonomy (FAULTY_COORDINATOR, FAULTY_PARTICIPANT, FAULTY_PARTICIPANT_OR_COORDINATOR, UNKNOWN_FAULTY_PARTICIPANT_OR_COORDINATOR, INVALID_INPUT). The public blame-reporting enum arrives with the Phase 3 API. State structs are fixed-size (no malloc, cap 128 participants). All secret temporaries are cleared; negligible-probability secret- dependent failures are declassified before branching. Documented deviations where the reference crashes with non-protocol errors: infinity sum_coms[0] in invalid_taproot_commit (unreachable after PoP verification) and tweak-hash overflow (negligible) return UNKNOWN_FAULTY_PARTICIPANT_OR_COORDINATOR; a wrong cmsg length maps to FAULTY_COORDINATOR. Investigation procedures are deferred to Phase 5 per the plan. tests_impl.h: byte-exact n=3/t=2 happy-path vectors for both layers generated from the Python reference (pmsg/cmsg/eq_input/enc_secshares and all DKG outputs), plus PoP reject cases (tampered, wrong index, infinity commitment, wrong echo, tampered non-constant-term sum), coordinator blame (bad commitment, overflowing encrypted share), encaps/decaps pad symmetry for all sender/receiver pairs including self-pad, encrypt->sum->decrypt_sum roundtrip, tampered encrypted share, invalid pubnonce blame, and input-validation rejects. Verified: make check 3/3 suites pass (incl. noverify_tests running the module); CMake ctest 361/361; ./tests --target=chilldkg runs all 9 module tests green.
2026-08-31 04:47:38 +02:00
chilldkg: Phase 1 - internal primitives (util, vss) Add the byte-exact internal primitives for the ChillDKG module, mirroring the Python reference implementation of the bip-frost-dkg draft (v0.3.0-dev), pinned to upstream commit a91896883f85b159415ecf298d5e844879af112d. util.h / util_impl.h (mirrors chilldkg_ref/util.py): - Point (de)serialization with explicit point-at-infinity support: 33 zero bytes <-> infinity, otherwise SEC compressed. Checked parse rejects invalid encodings and out-of-range x coordinates (point_save/point_load, xonly_save/xonly_load). - Internal parameterized-tag BIP-340 Schnorr sign/verify (chilldkg_schnorrsig_sign/_verify): tag prefix selects the <prefix>/aux, /nonce, /challenge subtags ("BIP DKG/pop message" for proofs of possession, "BIP0340" for CertEq signatures and recovery acks), arbitrary-length messages, pad33 zero-padding helper. The public schnorrsig API hardcodes BIP0340/32-byte messages, so the algorithm is replicated from secp256k1_schnorrsig_sign_internal with a custom tag; cross-checked against secp256k1_schnorrsig_sign32. - Tagged hashes via secp256k1_sha256_initialize_tagged: "BIP DKG/params_hash", "BIP DKG/encpedpop seed", "BIP DKG/simplpedpop aux", "BIP DKG/encpedpop secnonce", "BIP DKG/encpedpop ecdh", "BIP DKG/encaps_multi self_pad", "BIP DKG/vss coeffs", and BIP-341 "TapTweak" (32-byte x-only input). - params_hash = TH("BIP DKG/params_hash", u32be(t) || hostpubkeys) (note: plan had the operand order reversed; the reference hashes t first). - ECDH pads: reuses the ecdh module's SHA256-of-compressed-shared- point hash, then TH("BIP DKG/encpedpop ecdh", ecdh || sender_pubnonce || receiver_hostpubkey || context) with a sending flag fixing the sender|receiver order; self_pad for the own index. Pads are parsed wrapping (mod-n reduction); wire scalars, VSS coefficients and the TapTweak are parsed checked. vss.h / vss_impl.h (mirrors chilldkg_ref/vss.py): - vss_gen_coeffs: per-coefficient TH("BIP DKG/vss coeffs", seed || u32be(j)), checked parse with bitwise error accumulation. - vss_poly_eval (Horner) and vss_secshare_for with the x = id+1 convention (safe at UINT32_MAX). - vss_commit (constant-time ecmult_gen, zero coefficient -> infinity), vss_pubshare (powers-of-x over commitments, skips infinity), vss_commitment_add, vss_verify_secshare. - vss_invalid_taproot_commit: TapTweak applied to the x-only constant term so the Taproot script path is unspendable; returns tweak and pubtweak. tests_impl.h: 7 vector tests (tagged hashes, params_hash, point serialization incl. infinity roundtrip and parity prefixes, checked vs wrapping scalar parse at the group order boundary, custom-tag schnorrsig incl. wrong-tag/key/msg rejection, ECDH pad sender/receiver symmetry, VSS coeff derivation/Horner/commitment/pubshare/tweak) with expected values generated once from the Python reference (committed into the test file, reference commit recorded). Verified: make check 3/3 suites pass; CMake ctest all pass; ./tests --target=chilldkg runs all 7 new tests green in both verify and noverify builds.
2026-08-31 04:05:15 +02:00
/* This file contains the internal primitives of the ChillDKG module that
* mirror chilldkg_ref/util.py, secp256k1lab/bip340.py and secp256k1lab/ecdh.py
* of the bip-frost-dkg reference implementation. Byte-exactness with the
* reference is the goal; the tagged hash strings below are copied verbatim
* from the Python sources. */
/* Tagged hash initializers (BIP 340 tagged hashes). All ChillDKG tags share
* the "BIP DKG/" prefix (BIP_TAG in chilldkg_ref/util.py); "TapTweak" is the
* BIP 341 tag. */
static void secp256k1_chilldkg_sha256_tagged_params_hash(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha);
static void secp256k1_chilldkg_sha256_tagged_encpedpop_seed(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha);
static void secp256k1_chilldkg_sha256_tagged_simplpedpop_aux(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha);
static void secp256k1_chilldkg_sha256_tagged_encpedpop_secnonce(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha);
static void secp256k1_chilldkg_sha256_tagged_encpedpop_ecdh(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha);
static void secp256k1_chilldkg_sha256_tagged_self_pad(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha);
static void secp256k1_chilldkg_sha256_tagged_vss_coeffs(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha);
static void secp256k1_chilldkg_sha256_tagged_taptweak(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha);
/* Serialize a group element to 33 bytes: the point at infinity maps to 33
* zero bytes, otherwise the standard SEC compressed encoding (0x02/0x03
* prefix) is used. Mirrors GE.to_bytes_compressed_with_infinity. */
static void secp256k1_chilldkg_point_save(unsigned char *out33, const secp256k1_ge *p);
/* Parse a 33-byte group element encoding: 33 zero bytes map to the point at
* infinity, otherwise standard SEC compressed parsing applies. Returns 1 on
* success and 0 if the encoding is invalid (wrong prefix, x >= p, or x not on
* the curve). On failure, *p is set to the point at infinity. Mirrors
* GE.from_bytes_compressed_with_infinity. */
static int secp256k1_chilldkg_point_load(secp256k1_ge *p, const unsigned char *in33);
/* Serialize the x coordinate of a non-infinity group element (32 bytes).
* Mirrors GE.to_bytes_xonly, which asserts non-infinity. */
static void secp256k1_chilldkg_xonly_save(unsigned char *out32, const secp256k1_ge *p);
/* Parse a 32-byte x-only group element encoding (even y). Returns 1 on
* success, 0 if x >= p or x is not on the curve. Mirrors
* GE.from_bytes_xonly. */
static int secp256k1_chilldkg_xonly_load(secp256k1_ge *p, const unsigned char *in32);
/* Write str (at most 33 bytes) zero-padded to 33 bytes. Used to build the
* domain separation prefixes of the CertEq and recovery acknowledgment
* messages (see certeq_message/recovery_ack_message in chilldkg_ref). */
static void secp256k1_chilldkg_pad33(unsigned char *out33, const char *str);
/* BIP 340 Schnorr signing/verification with a parameterized tag prefix and
* arbitrary-length messages, mirroring secp256k1lab/bip340.py's schnorr_sign
* and schnorr_verify. The tags used for aux, nonce and challenge hashes are
* tag_prefix || "/aux", tag_prefix || "/nonce" and tag_prefix ||
* "/challenge". ChillDKG uses tag_prefix "BIP DKG/pop message" for proofs of
* possession (POP_MSG_TAG in simplpedpop.py) and the standard "BIP0340"
* prefix for CertEq and recovery acknowledgments.
*
* sign returns 1 on success and 0 if seckey32 is not in range 1..n-1 or the
* derived nonce is zero (negligible probability). On failure, sig64 is set to
* zero. verify returns 1 if the signature is valid and 0 otherwise. */
static int secp256k1_chilldkg_schnorrsig_sign(const secp256k1_context *ctx, unsigned char *sig64, const unsigned char *msg, size_t msglen, const unsigned char *seckey32, const unsigned char *aux_rand32, const char *tag_prefix);
static int secp256k1_chilldkg_schnorrsig_verify(const secp256k1_context *ctx, const unsigned char *sig64, const unsigned char *msg, size_t msglen, const unsigned char *pubkey32, const char *tag_prefix);
/* Compute the EncPedPop ECDH pad (encpedpop.py `ecdh`):
* shared = SHA256(compressed(seckey * their_point)) [libsecp256k1 ECDH]
* out = Scalar.from_bytes_wrapping(TH("BIP DKG/encpedpop ecdh",
* shared || my_pubkey33 || their_pubkey33 || context)) if sending
* out = Scalar.from_bytes_wrapping(TH("BIP DKG/encpedpop ecdh",
* shared || their_pubkey33 || my_pubkey33 || context)) otherwise
* In the protocol, my/their_pubkey33 are the sender's pubnonce and the
* receiver's host pubkey (in this order in the hash input, regardless of
* sending), and context is u32be(receiver_index) || enc_context.
* their_point must be the parsed their_pubkey33 and must not be infinity
* (enforced by VERIFY_CHECK). seckey must be nonzero. */
static void secp256k1_chilldkg_encpedpop_ecdh(const secp256k1_context *ctx, secp256k1_scalar *out, const secp256k1_scalar *seckey, const secp256k1_ge *their_point, const unsigned char *my_pubkey33, const unsigned char *their_pubkey33, const unsigned char *context, size_t context_len, int sending);
/* Compute the EncPedPop symmetric pad for encrypting to ourselves
* (encpedpop.py `self_pad`):
* out = Scalar.from_bytes_wrapping(TH("BIP DKG/encaps_multi self_pad",
* symkey32 || nonce33 || context))
* where symkey32 is the host seckey, nonce33 is the own pubnonce and context
* is u32be(own_index) || enc_context. */
static void secp256k1_chilldkg_encpedpop_self_pad(const secp256k1_hash_ctx *hash_ctx, secp256k1_scalar *out, const unsigned char *symkey32, const unsigned char *nonce33, const unsigned char *context, size_t context_len);
/* Compute the session parameters hash (chilldkg.py `params_hash`):
* out32 = TH("BIP DKG/params_hash", u32be(t) || hostpubkeys[0] || ... || hostpubkeys[n-1])
* where hostpubkeys33 is an array of n 33-byte compressed host public keys. */
chilldkg: Phase 3 - public participant API and CertEq Add the public participant-facing ChillDKG API to include/secp256k1_chilldkg.h and the CertEq sub-protocol, completing the participant side of the protocol (bip-frost-dkg v0.3.0-dev, reference pinned at a91896883f85b159415ecf298d5e844879af112d). New module files: - certeq.h / certeq_impl.h: CertEq sub-protocol. Participants sign pad33("BIP DKG/certeq message") || u32be(i) || eq_input with plain BIP0340-tagged Schnorr signatures under their host key (certeq_participant_step); verification is per-index against the x-only hostpubkeys[i][1:33] exactly as the reference (certeq_verify). The coordinator side reuses certeq_verify in Phase 4. Public API (all no-malloc, caller-allocated buffers, outputs zeroed on failure, secret paths cleared): - secp256k1_chilldkg_hostpubkey_gen: plain compressed host pubkey generation; rejects zero / >= group order seckeys. - secp256k1_chilldkg_params_hash: validates session params (participant and threshold ranges, strictly compressed non-infinity pubkeys, no duplicates) and computes TH("BIP DKG/params_hash", u32be(t) || hostpubkeys). - Message-length helpers so callers can size buffers: participant_msg1_len (33t+32n+97), coordinator_msg1_len (162n+33(t-1)), participant_msg2_len (64), coordinator_msg2_len (64n), recovery_data_len (4+33t+162n). - secp256k1_chilldkg_participant_step1: full EncPedPop step1 with seed=deckey=hostseckey; rejects zero randomness and hostseckeys not matching the claimed hostpubkey (input errors, not protocol faults). - secp256k1_chilldkg_participant_step2: parses and verifies cmsg1 via the Phase 2 encpedpop/simplpedpop participant path, computes the tweaked secshare/pubshares/threshold pubkey, appends enc_secshares to eq_input (matching the reference for recovery consistency), and emits the 64-byte CertEq signature. - secp256k1_chilldkg_participant_finalize: re-verifies all n CertEq signatures in the certificate, then outputs the 32-byte secshare, 33-byte threshold pubkey, n pubshares and the self-delimiting recovery data (eq_input || cert). Blame reporting without exceptions: public enum secp256k1_chilldkg_fault (OK / FAULTY_COORDINATOR / FAULTY_PARTICIPANT / FAULTY_PARTICIPANT_OR_COORDINATOR / UNKNOWN_FAULTY_PARTICIPANT_OR_COORDINATOR / INVALID_INPUT) plus an out fault_index, mapping the reference's exception taxonomy: - hostseckey invalid/mismatch -> INVALID_INPUT (HostSeckeyError), - cmsg1 scalar overflow/parse -> FAULTY_COORDINATOR (MsgParseError), - pubnonce/commitment/PoP faults -> FAULTY_PARTICIPANT_OR_COORDINATOR(i), - share-vs-pubshare mismatch -> UNKNOWN with fault_index = UINT32_MAX, - certificate signature failure -> FAULTY_COORDINATOR (documented deviation: fault_index carries the failing signature index as diagnostic info; the reference discards it). Enum-returning functions use a local CHILLDKG_ARG_CHECK that fires the illegal-argument callback and returns INVALID_INPUT (ARG_CHECK would return 0 = OK). Opaque state objects with magic-validated save/load (frost idiom): participant_state1 (4306 bytes, no secrets) and participant_state2 (21073 bytes, contains the secshare; documented keep-secret/no-copy). Fixed-size at SECP256K1_CHILLDKG_MAX_PARTICIPANTS = 128. Also fixes a noverify-build bug: state1_load ran point_load inside VERIFY_CHECK, which compiles out in noverify builds and left the commitment uninitialized; now called unconditionally. tests_impl.h: participant_api_test with full-session reference vectors (n=3, t=2; coordinator aggregation simulated through the internal Phase 2 coordinator step and verified byte-identical to the reference's coordinator_step1): msglen helpers, hostpubkey_gen and params_hash vectors incl. duplicate/invalid/infinity rejection, byte-exact pmsg1/cmsg1/CertEq sigs/secshare/thresh_pk/pubshares/ recovery, blame cases (tampered enc_secshare -> UNKNOWN, invalid pubnonce -> FAULTY_PARTICIPANT_OR_COORDINATOR(1), overflowing enc_secshare -> FAULTY_COORDINATOR, corrupted cert sig -> FAULTY_COORDINATOR with fault_index and zeroed outputs), NULL-arg misuse and bad-magic state rejection. Verified: make check 3/3 (incl. noverify); CMake ctest 363/363; make distdir includes all new files.
2026-08-31 05:22:53 +02:00
static void secp256k1_chilldkg_params_hash_internal(const secp256k1_hash_ctx *hash_ctx, unsigned char *out32, const unsigned char *hostpubkeys33, size_t n, uint32_t t);
chilldkg: Phase 1 - internal primitives (util, vss) Add the byte-exact internal primitives for the ChillDKG module, mirroring the Python reference implementation of the bip-frost-dkg draft (v0.3.0-dev), pinned to upstream commit a91896883f85b159415ecf298d5e844879af112d. util.h / util_impl.h (mirrors chilldkg_ref/util.py): - Point (de)serialization with explicit point-at-infinity support: 33 zero bytes <-> infinity, otherwise SEC compressed. Checked parse rejects invalid encodings and out-of-range x coordinates (point_save/point_load, xonly_save/xonly_load). - Internal parameterized-tag BIP-340 Schnorr sign/verify (chilldkg_schnorrsig_sign/_verify): tag prefix selects the <prefix>/aux, /nonce, /challenge subtags ("BIP DKG/pop message" for proofs of possession, "BIP0340" for CertEq signatures and recovery acks), arbitrary-length messages, pad33 zero-padding helper. The public schnorrsig API hardcodes BIP0340/32-byte messages, so the algorithm is replicated from secp256k1_schnorrsig_sign_internal with a custom tag; cross-checked against secp256k1_schnorrsig_sign32. - Tagged hashes via secp256k1_sha256_initialize_tagged: "BIP DKG/params_hash", "BIP DKG/encpedpop seed", "BIP DKG/simplpedpop aux", "BIP DKG/encpedpop secnonce", "BIP DKG/encpedpop ecdh", "BIP DKG/encaps_multi self_pad", "BIP DKG/vss coeffs", and BIP-341 "TapTweak" (32-byte x-only input). - params_hash = TH("BIP DKG/params_hash", u32be(t) || hostpubkeys) (note: plan had the operand order reversed; the reference hashes t first). - ECDH pads: reuses the ecdh module's SHA256-of-compressed-shared- point hash, then TH("BIP DKG/encpedpop ecdh", ecdh || sender_pubnonce || receiver_hostpubkey || context) with a sending flag fixing the sender|receiver order; self_pad for the own index. Pads are parsed wrapping (mod-n reduction); wire scalars, VSS coefficients and the TapTweak are parsed checked. vss.h / vss_impl.h (mirrors chilldkg_ref/vss.py): - vss_gen_coeffs: per-coefficient TH("BIP DKG/vss coeffs", seed || u32be(j)), checked parse with bitwise error accumulation. - vss_poly_eval (Horner) and vss_secshare_for with the x = id+1 convention (safe at UINT32_MAX). - vss_commit (constant-time ecmult_gen, zero coefficient -> infinity), vss_pubshare (powers-of-x over commitments, skips infinity), vss_commitment_add, vss_verify_secshare. - vss_invalid_taproot_commit: TapTweak applied to the x-only constant term so the Taproot script path is unspendable; returns tweak and pubtweak. tests_impl.h: 7 vector tests (tagged hashes, params_hash, point serialization incl. infinity roundtrip and parity prefixes, checked vs wrapping scalar parse at the group order boundary, custom-tag schnorrsig incl. wrong-tag/key/msg rejection, ECDH pad sender/receiver symmetry, VSS coeff derivation/Horner/commitment/pubshare/tweak) with expected values generated once from the Python reference (committed into the test file, reference commit recorded). Verified: make check 3/3 suites pass; CMake ctest all pass; ./tests --target=chilldkg runs all 7 new tests green in both verify and noverify builds.
2026-08-31 04:05:15 +02:00
#endif