Files
secp256k1-zkp/tools/test_vectors_chilldkg_generate.py
Kgothatso Ngako d1c817b528 chilldkg: Phase 6 - test vectors, FROST integration, docs, example
Final phase of the ChillDKG module: upstream test vectors, a
DKG->FROST integration test, boundary tests, full module
documentation and a runnable example.

Test vectors:
- tools/test_vectors_chilldkg_generate.py converts all 10 upstream
  bip-frost-dkg JSON vector files into src/modules/chilldkg/vectors.h
  (modeled on tools/test_vectors_frost_generate.py; takes the vectors
  directory as an argument; upstream pinned to commit
  a91896883f85b159415ecf298d5e844879af112d, recorded in the generated
  header with the exact regeneration invocation; regeneration is
  reproducible byte-for-byte).
- tests_impl.h vector runners execute 191 of 241 upstream cases
  through the public API: hostpubkey_gen, params_hash,
  participant_step1/step2/finalize/investigate,
  coordinator_step1/finalize/investigate, recover. Happy paths are
  byte-exact (pmsg1/cmsg1/pmsg2/cmsg2/dkg_output/recovery/cinv);
  error cases assert both the fault enum and fault_index against
  expectedError.participantId. The 50 skipped cases are
  wrong-length/wrong-count inputs not expressible with the
  fixed-size C API; each skip is documented in vectors.h.

Boundary/robustness tests: t=1, t=n, n=2, a full n=128/t=2 session
end-to-end with per-participant secshare*G == pubshare checks and a
recovery roundtrip, and a state1 memcpy roundtrip (step2 from a copied
state object).

DKG->FROST integration test (guarded by ENABLE_MODULE_FROST): a full
ChillDKG session (n=3, t=2) feeds (secshare, thresh_pk, pubshares)
directly into the frost module. ChillDKG's thresh_pk is already
TapTweak'ed, so frost_tweak_cache_init is called with no further
tweaks (frost's tweaked x-only key asserted equal to the x-only part
of the ChillDKG thresh_pk); signers 0 and 2 run nonce_gen, nonce_agg,
session_init with the shared x = id+1 convention, frost_sign,
partial_sig_verify and partial_sig_agg; the aggregate signature
verifies as a plain BIP-340 signature against the threshold key.

Example: examples/chilldkg.c runs a full 2-of-3 DKG session (host key
generation, params hash, participant/coordinator steps, finalize, and
a recovery roundtrip via participant_recover) with fixed-size buffers
and secret erasure. Wired into Makefile.am and
examples/CMakeLists.txt exactly like frost_example (runs as a TEST);
chilldkg_example binary added to .gitignore.

Docs: src/modules/chilldkg/chilldkg.md now documents the protocol
summary, message-flow table with exact byte sizes, blame taxonomy,
recovery workflow, security notes (host key reuse/retention, fresh
randomness per session, state secrecy, recovery-data sensitivity) and
the pinned reference commit; src/modules/frost/frost.md points at the
new module as the intended DKG.

Bug fix found by the vector runner (recover tcId 9): the internal
recover() passed a possibly-NULL fault_index from coordinator_recover
to certeq_verify, which dereferences it on failure; now uses a local.

Verified: make check 10/10 (3 test suites + 7 examples incl.
chilldkg_example, exit 0 when run); CMake ctest 428/428 with chilldkg
+ frost, and a no-frost build confirms the ENABLE_MODULE_FROST guard;
make distdir includes vectors.h, the example and the generator.

The module is feature-complete against bip-frost-dkg v0.3.0-dev at
a91896883f85b159415ecf298d5e844879af112d. The BIP is still a draft;
tagged hashes and wire formats may change upstream.
2026-08-31 06:52:58 +02:00

725 lines
26 KiB
Python

#!/usr/bin/env python3
"""Converts the ChillDKG test vectors of the bip-frost-dkg reference
repository into a C header used by the tests in src/modules/chilldkg/tests_impl.h.
Usage: tools/test_vectors_chilldkg_generate.py <dir>
<dir> must contain the vector files of the bip-frost-dkg repository
(https://github.com/BlockstreamResearch/bip-frost-dkg, directory vectors/)
pinned at commit a91896883f85b159415ecf298d5e844879af112d.
"""
import sys
import json
import textwrap
if len(sys.argv) < 2:
print(__doc__)
sys.exit(1)
VEC_DIR = sys.argv[1]
FILES = {
"hostpubkey_gen": "hostpubkey_gen_vectors.json",
"params_hash": "params_hash_vectors.json",
"participant_step1": "participant_step1_vectors.json",
"participant_step2": "participant_step2_vectors.json",
"participant_finalize": "participant_finalize_vectors.json",
"participant_investigate": "participant_investigate_vectors.json",
"coordinator_step1": "coordinator_step1_vectors.json",
"coordinator_finalize": "coordinator_finalize_vectors.json",
"coordinator_investigate": "coordinator_investigate_vectors.json",
"recover": "recover_vectors.json",
}
data = {k: json.load(open(VEC_DIR + "/" + f)) for k, f in FILES.items()}
skipped_cases = []
def hexstr_to_intarray(s):
# Always emitted inside brace initializers. C90 forbids empty initializer
# braces, so an empty byte string becomes "0" (the associated length field
# is 0, so the padding byte is never read).
return ", ".join([f"0x{b:02X}" for b in bytes.fromhex(s)]) or "0"
def byte_array(hex_str):
return "{ %s }" % hexstr_to_intarray(hex_str)
def indent(s, level=1):
return textwrap.indent(s, 4 * level * " ")
def map_error(case, step_name):
"""Maps the reference expectedError of an error case to a
chilldkg_vec_error code and fault index. Returns None if the case is not
expressible in the C API (wrong-length or wrong-count inputs; the C API
uses fixed-size buffers whose lengths derive from the session
parameters)."""
err = case["expectedError"]
etype = err["type"]
if etype == "ValueError":
skipped_cases.append(
"%s tcId %d (%s): not expressible in the C API (wrong input length or count)"
% (step_name, case["tcId"], case.get("comment", "").strip())
)
return None
fault_index = "UINT32_MAX"
if "participantId" in err:
assert etype in ("FaultyParticipantError", "FaultyParticipantOrCoordinatorError", "InvalidHostPubkeyError", "DuplicateHostPubkeyError")
fault_index = str(err["participantId"])
if etype in ("HostSeckeyError", "ThresholdOrCountError", "InvalidHostPubkeyError",
"DuplicateHostPubkeyError", "RandomnessError", "RecoveryDataError"):
return "CHILLDKG_VEC_INVALID_INPUT", "UINT32_MAX"
if etype == "FaultyCoordinatorError":
return "CHILLDKG_VEC_FAULTY_COORDINATOR", fault_index
if etype == "FaultyParticipantError":
return "CHILLDKG_VEC_FAULTY_PARTICIPANT", fault_index
if etype == "FaultyParticipantOrCoordinatorError":
return "CHILLDKG_VEC_FAULTY_PARTICIPANT_OR_COORDINATOR", fault_index
if etype == "UnknownFaultyParticipantOrCoordinatorError":
return "CHILLDKG_VEC_UNKNOWN_FAULTY_PARTICIPANT_OR_COORDINATOR", fault_index
sys.exit("Unknown error type %s in %s tcId %d" % (etype, step_name, case["tcId"]))
def groups(d):
return d["testGroups"] if "testGroups" in d else [d]
def all_cases(d):
for g in groups(d):
for c in g.get("validTestCases", []) + g.get("errorTestCases", []):
yield g, c
def get_params(case, group):
return case.get("params", group.get("params"))
# Compute the shared maximum sizes.
max_n = 0
max_t = 0
max_pmsg1 = 0
max_cmsg1 = 0
max_cmsg2 = 0
max_recovery = 0
max_cinv = 0
max_pmsg1_pool = 0
max_pmsg2_pool = 0
max_cmsg1_pool = 0
max_indices = 0
for name, d in data.items():
for g, c in all_cases(d):
p = get_params(c, g)
if p is not None:
max_n = max(max_n, len(p["hostpubkeys"]))
max_t = max(max_t, p["t"])
for key, cur in (("expectedPmsg1", "pmsg1"), ("cmsg1", "cmsg1"), ("cmsg2", "cmsg2"),
("recoveryData", "recovery"), ("cinvMsg", "cinv")):
v = c.get(key)
if isinstance(v, str):
if cur == "pmsg1":
max_pmsg1 = max(max_pmsg1, len(v) // 2)
elif cur == "cmsg1":
max_cmsg1 = max(max_cmsg1, len(v) // 2)
elif cur == "cmsg2":
max_cmsg2 = max(max_cmsg2, len(v) // 2)
elif cur == "recovery":
max_recovery = max(max_recovery, len(v) // 2)
elif cur == "cinv":
max_cinv = max(max_cinv, len(v) // 2)
if "pmsg1Indices" in c:
max_indices = max(max_indices, len(c["pmsg1Indices"]))
if "pmsg2Indices" in c:
max_indices = max(max_indices, len(c["pmsg2Indices"]))
for g in groups(d):
if "pmsg1" in g and isinstance(g["pmsg1"], str):
max_pmsg1 = max(max_pmsg1, len(g["pmsg1"]) // 2)
if "cmsg1" in g and isinstance(g["cmsg1"], str):
max_cmsg1 = max(max_cmsg1, len(g["cmsg1"]) // 2)
if "pmsgs1" in g:
max_n = max(max_n, len(g["pmsgs1"]))
for m in g["pmsgs1"]:
max_pmsg1 = max(max_pmsg1, len(m) // 2)
if "pmsg1Pool" in g:
max_pmsg1_pool = max(max_pmsg1_pool, len(g["pmsg1Pool"]))
for m in g["pmsg1Pool"]:
max_pmsg1 = max(max_pmsg1, len(m) // 2)
if "pmsg2Pool" in g:
max_pmsg2_pool = max(max_pmsg2_pool, len(g["pmsg2Pool"]))
if "cmsg1Pool" in g:
max_cmsg1_pool = max(max_cmsg1_pool, len(g["cmsg1Pool"]))
for m in g["cmsg1Pool"]:
max_cmsg1 = max(max_cmsg1, len(m) // 2)
for c in g.get("validTestCases", []):
if "expectedCinvMsgs" in c:
for m in c["expectedCinvMsgs"]:
max_cinv = max(max_cinv, len(m) // 2)
eo = c.get("expectedOutput")
if eo is not None:
if "recoveryData" in eo:
max_recovery = max(max_recovery, len(eo["recoveryData"]) // 2)
if "cmsg2" in eo:
max_cmsg2 = max(max_cmsg2, len(eo["cmsg2"]) // 2)
for c in g.get("validTestCases", []) + g.get("errorTestCases", []):
if isinstance(c.get("recoveryData"), str):
max_recovery = max(max_recovery, len(c["recoveryData"]) // 2)
s = """/**
* Automatically generated by %s.
*
* The test vectors are from the bip-frost-dkg reference repository
* https://github.com/BlockstreamResearch/bip-frost-dkg, pinned at commit
* a91896883f85b159415ecf298d5e844879af112d (BIP version 0.3.0-dev). They are
* used by the tests in src/modules/chilldkg/tests_impl.h.
*
* Regenerate with:
* tools/test_vectors_chilldkg_generate.py <dir> > src/modules/chilldkg/vectors.h
* where <dir> is the vectors/ directory of the reference repository at the
* commit above. */
#ifndef SECP256K1_MODULE_CHILLDKG_VECTORS_H
#define SECP256K1_MODULE_CHILLDKG_VECTORS_H
#include <stddef.h>
#include <stdint.h>
""" % sys.argv[0]
s += """/* The expected result of an error test case, mapped from the reference
* exception taxonomy; the values coincide with secp256k1_chilldkg_fault.
* CHILLDKG_VEC_INVALID_INPUT covers the local input errors (ValueError
* subclasses that are not protocol faults: HostSeckeyError,
* ThresholdOrCountError, InvalidHostPubkeyError, DuplicateHostPubkeyError,
* RandomnessError, RecoveryDataError). */
enum chilldkg_vec_error {
CHILLDKG_VEC_OK = 0,
CHILLDKG_VEC_FAULTY_COORDINATOR = 1,
CHILLDKG_VEC_FAULTY_PARTICIPANT = 2,
CHILLDKG_VEC_FAULTY_PARTICIPANT_OR_COORDINATOR = 3,
CHILLDKG_VEC_UNKNOWN_FAULTY_PARTICIPANT_OR_COORDINATOR = 4,
CHILLDKG_VEC_INVALID_INPUT = 5
};
enum {
CHILLDKG_VEC_MAX_PARTICIPANTS = %d,
CHILLDKG_VEC_MAX_PMSG1_LEN = %d,
CHILLDKG_VEC_MAX_CMSG1_LEN = %d,
CHILLDKG_VEC_MAX_CMSG2_LEN = %d,
CHILLDKG_VEC_MAX_RECOVERY_LEN = %d,
CHILLDKG_VEC_MAX_CINV_LEN = %d,
CHILLDKG_VEC_MAX_PMSG1_POOL = %d,
CHILLDKG_VEC_MAX_PMSG2_POOL = %d,
CHILLDKG_VEC_MAX_CMSG1_POOL = %d,
CHILLDKG_VEC_MAX_INDICES = %d
};
/* Session parameters (hostpubkeys || t). */
struct chilldkg_vec_params {
uint32_t t;
size_t n;
unsigned char hostpubkeys[CHILLDKG_VEC_MAX_PARTICIPANTS][33];
};
/* The DKG output of a participant or the coordinator (has_secshare == 0). */
struct chilldkg_vec_dkg_output {
int has_secshare;
unsigned char secshare[32];
unsigned char thresh_pk[33];
unsigned char pubshares[CHILLDKG_VEC_MAX_PARTICIPANTS][33];
};
""" % (max_n, max_pmsg1, max_cmsg1, max_cmsg2, max_recovery, max_cinv,
max_pmsg1_pool, max_pmsg2_pool, max_cmsg1_pool, max_indices)
def init_params(p):
inner = "\n" + indent(",\n".join(byte_array(h) for h in p["hostpubkeys"]), 2) + "\n"
return "{ %d, %d, {%s} }" % (p["t"], len(p["hostpubkeys"]), inner)
def init_dkg_output(o):
return "{ %d, %s, %s, {\n%s\n} }" % (
0 if o["secshare"] is None else 1,
byte_array(o["secshare"]) if o["secshare"] is not None else "{ 0 }",
byte_array(o["threshPk"]),
indent(",\n".join(byte_array(p) for p in o["pubshares"]), 2),
)
def case_list(d):
"""Returns the (is_valid, case) list of a vector file (flat per-group
files) with inexpressible error cases removed."""
out = []
for g in groups(d):
for c in g.get("validTestCases", []):
out.append((1, c))
for c in g.get("errorTestCases", []):
out.append((0, c))
return out
# hostpubkey_gen vectors
cases = []
for is_valid, c in case_list(data["hostpubkey_gen"]):
if not is_valid and map_error(c, "hostpubkey_gen") is None:
continue
cases.append((is_valid, c))
s += """
struct chilldkg_vec_hostpubkey_gen_case {
int is_valid;
unsigned char hostseckey[32];
unsigned char expected_hostpubkey[33];
};
static const struct chilldkg_vec_hostpubkey_gen_case chilldkg_vec_hostpubkey_gen_cases[%d] = {
""" % len(cases)
for is_valid, c in cases:
s += indent("{ %d, %s, %s },\n" % (
is_valid,
byte_array(c["hostseckey"]),
byte_array(c.get("expectedHostpubkey") or ""),
))
s += "};\n"
# params_hash vectors
cases = []
for is_valid, c in case_list(data["params_hash"]):
if not is_valid and map_error(c, "params_hash") is None:
continue
cases.append((is_valid, c))
s += """
struct chilldkg_vec_params_hash_case {
int is_valid;
struct chilldkg_vec_params params;
unsigned char expected_hash[32];
};
static const struct chilldkg_vec_params_hash_case chilldkg_vec_params_hash_cases[%d] = {
""" % len(cases)
for is_valid, c in cases:
s += indent("{ %d,\n%s,\n%s },\n" % (
is_valid,
indent(init_params(c["params"]) + ",", 1)[:-1],
byte_array(c.get("expectedParamsHash") or ""),
))
s += "};\n"
# participant_step1 vectors
cases = []
for is_valid, c in case_list(data["participant_step1"]):
if not is_valid and map_error(c, "participant_step1") is None:
continue
cases.append((is_valid, c))
s += """
struct chilldkg_vec_step1_case {
int is_valid;
unsigned char hostseckey[32];
struct chilldkg_vec_params params;
unsigned char random[32];
unsigned char expected_pmsg1[CHILLDKG_VEC_MAX_PMSG1_LEN];
};
static const struct chilldkg_vec_step1_case chilldkg_vec_step1_cases[%d] = {
""" % len(cases)
for is_valid, c in cases:
s += indent("{ %d, %s,\n%s,\n%s, %s },\n" % (
is_valid,
byte_array(c["hostseckey"]),
indent(init_params(c["params"]) + ",", 1)[:-1],
byte_array(c["random"]),
byte_array(c.get("expectedPmsg1") or ""),
))
s += "};\n"
# participant_step2 vectors
s += """
struct chilldkg_vec_step2_group {
struct chilldkg_vec_params params;
unsigned char hostseckey[32];
unsigned char random[32];
unsigned char aux_rand[32];
unsigned char pmsg1[CHILLDKG_VEC_MAX_PMSG1_LEN];
};
struct chilldkg_vec_step2_case {
int is_valid;
/* hostseckey override (error cases only); has_hostseckey == 0 means the
* group's hostseckey is used. */
int has_hostseckey;
unsigned char hostseckey[32];
unsigned char cmsg1[CHILLDKG_VEC_MAX_CMSG1_LEN];
/* Expected result: CHILLDKG_VEC_OK or an enum chilldkg_vec_error value,
* mapped to secp256k1_chilldkg_fault / return-0 as in the tests. */
int expected_error;
uint32_t expected_fault_index;
unsigned char expected_pmsg2[64];
};
"""
s += "static const struct chilldkg_vec_step2_group chilldkg_vec_step2_groups[%d] = {\n" % len(groups(data["participant_step2"]))
for g in groups(data["participant_step2"]):
s += indent("{\n%s,\n%s, %s, %s, %s },\n" % (
indent(init_params(g["params"]) + ",", 1)[:-1],
byte_array(g["hostseckey"]),
byte_array(g["random"]),
byte_array(g["auxRand"]),
byte_array(g["pmsg1"]),
))
s += "};\n"
step2_cases = []
for gi, g in enumerate(groups(data["participant_step2"])):
for c in g.get("validTestCases", []):
step2_cases.append((gi, 1, c))
for c in g.get("errorTestCases", []):
if map_error(c, "participant_step2") is None:
continue
step2_cases.append((gi, 0, c))
s += "static const struct chilldkg_vec_step2_case chilldkg_vec_step2_cases[%d] = {\n" % len(step2_cases)
for gi, is_valid, c in step2_cases:
if is_valid:
error_code, fault_index = "CHILLDKG_VEC_OK", "UINT32_MAX"
expected = byte_array(c["expectedPmsg2"])
else:
error_code, fault_index = map_error(c, "participant_step2")
expected = "{ 0 }"
has_hsk = 1 if "hostseckey" in c else 0
s += indent("{ %d, %d, %s,\n%s,\n%s, %s, %s },\n" % (
is_valid,
has_hsk,
byte_array(c.get("hostseckey") or "00" * 32),
indent(byte_array(c["cmsg1"]) + ",", 1)[:-1],
error_code, fault_index, expected,
))
# Note: the group index is implicit (cases are emitted group by group).
s += "};\n"
s += "static const size_t chilldkg_vec_step2_group_case_counts[%d] = {\n" % len(groups(data["participant_step2"]))
gi = 0
for g in groups(data["participant_step2"]):
cnt = sum(1 for x in step2_cases if x[0] == gi)
s += indent("%d,\n" % cnt)
gi += 1
s += "};\n"
# participant_finalize vectors
s += """
struct chilldkg_vec_finalize_group {
struct chilldkg_vec_params params;
unsigned char hostseckey[32];
unsigned char random[32];
unsigned char aux_rand[32];
unsigned char pmsg1[CHILLDKG_VEC_MAX_PMSG1_LEN];
unsigned char cmsg1[CHILLDKG_VEC_MAX_CMSG1_LEN];
unsigned char pmsg2[64];
};
struct chilldkg_vec_finalize_case {
int is_valid;
unsigned char cmsg2[CHILLDKG_VEC_MAX_CMSG2_LEN];
/* Expected result: CHILLDKG_VEC_OK or CHILLDKG_VEC_FAULTY_COORDINATOR. */
int expected_error;
struct chilldkg_vec_dkg_output expected_output;
unsigned char expected_recovery[CHILLDKG_VEC_MAX_RECOVERY_LEN];
};
"""
s += "static const struct chilldkg_vec_finalize_group chilldkg_vec_finalize_groups[%d] = {\n" % len(groups(data["participant_finalize"]))
for g in groups(data["participant_finalize"]):
s += indent("{\n%s,\n%s, %s, %s, %s,\n%s,\n%s },\n" % (
indent(init_params(g["params"]) + ",", 1)[:-1],
byte_array(g["hostseckey"]),
byte_array(g["random"]),
byte_array(g["auxRand"]),
byte_array(g["pmsg1"]),
indent(byte_array(g["cmsg1"]) + ",", 1)[:-1],
byte_array(g["pmsg2"]),
))
s += "};\n"
finalize_cases = []
for gi, g in enumerate(groups(data["participant_finalize"])):
for c in g.get("validTestCases", []):
finalize_cases.append((gi, 1, c))
for c in g.get("errorTestCases", []):
if map_error(c, "participant_finalize") is None:
continue
finalize_cases.append((gi, 0, c))
s += "static const struct chilldkg_vec_finalize_case chilldkg_vec_finalize_cases[%d] = {\n" % len(finalize_cases)
for gi, is_valid, c in finalize_cases:
if is_valid:
error_code = "CHILLDKG_VEC_OK"
eo = init_dkg_output(c["expectedOutput"]["dkgOutput"])
rec = byte_array(c["expectedOutput"]["recoveryData"])
else:
error_code, _ = map_error(c, "participant_finalize")
eo = "{ 0, { 0 }, { 0 }, { { 0 } } }"
rec = "{ 0 }"
s += indent("{ %d,\n%s,\n%s,\n%s,\n%s },\n" % (
is_valid,
indent(byte_array(c["cmsg2"]) + ",", 1)[:-1],
error_code,
indent(eo + ",", 1)[:-1],
indent(rec, 1),
))
s += "};\n"
s += "static const size_t chilldkg_vec_finalize_group_case_counts[%d] = {\n" % len(groups(data["participant_finalize"]))
for gi in range(len(groups(data["participant_finalize"]))):
cnt = sum(1 for x in finalize_cases if x[0] == gi)
s += indent("%d,\n" % cnt)
s += "};\n"
# participant_investigate vectors
s += """
struct chilldkg_vec_investigate_group {
struct chilldkg_vec_params params;
unsigned char hostseckey[32];
unsigned char random[32];
unsigned char aux_rand[32];
unsigned char pmsg1[CHILLDKG_VEC_MAX_PMSG1_LEN];
size_t n_cmsg1;
unsigned char cmsg1_pool[CHILLDKG_VEC_MAX_CMSG1_POOL][CHILLDKG_VEC_MAX_CMSG1_LEN];
};
struct chilldkg_vec_investigate_case {
size_t cmsg1_index;
unsigned char cinv[CHILLDKG_VEC_MAX_CINV_LEN];
/* Expected result of participant_investigate. */
int expected_error;
uint32_t expected_fault_index;
};
"""
s += "static const struct chilldkg_vec_investigate_group chilldkg_vec_investigate_groups[%d] = {\n" % len(groups(data["participant_investigate"]))
for g in groups(data["participant_investigate"]):
pool = g["cmsg1Pool"]
inner = "\n" + indent(",\n".join(byte_array(m) for m in pool), 2) + "\n"
s += indent("{\n%s,\n%s, %s, %s, %s,\n%s, {%s} },\n" % (
indent(init_params(g["params"]) + ",", 1)[:-1],
byte_array(g["hostseckey"]),
byte_array(g["random"]),
byte_array(g["auxRand"]),
byte_array(g["pmsg1"]),
len(pool),
inner,
))
s += "};\n"
investigate_cases = []
for gi, g in enumerate(groups(data["participant_investigate"])):
for c in g.get("errorTestCases", []):
investigate_cases.append((gi, c))
s += "static const struct chilldkg_vec_investigate_case chilldkg_vec_investigate_cases[%d] = {\n" % len(investigate_cases)
for gi, c in investigate_cases:
error_code, fault_index = map_error(c, "participant_investigate")
s += indent("{ %d,\n%s,\n%s, %s },\n" % (
c["cmsg1Index"],
indent(byte_array(c["cinvMsg"]) + ",", 1)[:-1],
error_code, fault_index,
))
s += "};\n"
s += "static const size_t chilldkg_vec_investigate_group_case_counts[%d] = {\n" % len(groups(data["participant_investigate"]))
for gi in range(len(groups(data["participant_investigate"]))):
cnt = sum(1 for x in investigate_cases if x[0] == gi)
s += indent("%d,\n" % cnt)
s += "};\n"
# coordinator_step1 vectors
s += """
struct chilldkg_vec_coord_step1_group {
size_t n_pmsg1_pool;
unsigned char pmsg1_pool[CHILLDKG_VEC_MAX_PMSG1_POOL][CHILLDKG_VEC_MAX_PMSG1_LEN];
};
struct chilldkg_vec_coord_step1_case {
int is_valid;
size_t n_pmsgs1;
size_t pmsg1_indices[CHILLDKG_VEC_MAX_INDICES];
struct chilldkg_vec_params params;
/* Expected result: CHILLDKG_VEC_OK or an enum chilldkg_vec_error value. */
int expected_error;
uint32_t expected_fault_index;
unsigned char expected_cmsg1[CHILLDKG_VEC_MAX_CMSG1_LEN];
};
"""
s += "static const struct chilldkg_vec_coord_step1_group chilldkg_vec_coord_step1_groups[%d] = {\n" % len(groups(data["coordinator_step1"]))
for g in groups(data["coordinator_step1"]):
pool = g["pmsg1Pool"]
inner = "\n" + indent(",\n".join(byte_array(m) for m in pool), 2) + "\n"
s += indent("{ %d, {%s} },\n" % (len(pool), inner))
s += "};\n"
coord_step1_cases = []
for gi, g in enumerate(groups(data["coordinator_step1"])):
for c in g.get("validTestCases", []):
coord_step1_cases.append((gi, 1, c))
for c in g.get("errorTestCases", []):
if map_error(c, "coordinator_step1") is None:
continue
coord_step1_cases.append((gi, 0, c))
s += "static const struct chilldkg_vec_coord_step1_case chilldkg_vec_coord_step1_cases[%d] = {\n" % len(coord_step1_cases)
for gi, is_valid, c in coord_step1_cases:
if is_valid:
error_code, fault_index = "CHILLDKG_VEC_OK", "UINT32_MAX"
expected = byte_array(c["expectedCmsg1"])
else:
error_code, fault_index = map_error(c, "coordinator_step1")
expected = "{ 0 }"
s += indent("{ %d, %d, { %s },\n%s,\n%s, %s,\n%s },\n" % (
is_valid,
len(c["pmsg1Indices"]),
", ".join(map(str, c["pmsg1Indices"])),
indent(init_params(c["params"]) + ",", 1)[:-1],
error_code, fault_index,
indent(expected, 1),
))
s += "};\n"
s += "static const size_t chilldkg_vec_coord_step1_group_case_counts[%d] = {\n" % len(groups(data["coordinator_step1"]))
for gi in range(len(groups(data["coordinator_step1"]))):
cnt = sum(1 for x in coord_step1_cases if x[0] == gi)
s += indent("%d,\n" % cnt)
s += "};\n"
# coordinator_finalize vectors
s += """
struct chilldkg_vec_coord_finalize_group {
struct chilldkg_vec_params params;
unsigned char pmsgs1[CHILLDKG_VEC_MAX_PARTICIPANTS][CHILLDKG_VEC_MAX_PMSG1_LEN];
unsigned char cmsg1[CHILLDKG_VEC_MAX_CMSG1_LEN];
size_t n_pmsg2_pool;
unsigned char pmsg2_pool[CHILLDKG_VEC_MAX_PMSG2_POOL][64];
};
struct chilldkg_vec_coord_finalize_case {
int is_valid;
size_t n_pmsgs2;
size_t pmsg2_indices[CHILLDKG_VEC_MAX_INDICES];
/* Expected result: CHILLDKG_VEC_OK or CHILLDKG_VEC_FAULTY_PARTICIPANT. */
int expected_error;
uint32_t expected_fault_index;
unsigned char expected_cmsg2[CHILLDKG_VEC_MAX_CMSG2_LEN];
struct chilldkg_vec_dkg_output expected_output;
unsigned char expected_recovery[CHILLDKG_VEC_MAX_RECOVERY_LEN];
};
"""
s += "static const struct chilldkg_vec_coord_finalize_group chilldkg_vec_coord_finalize_groups[%d] = {\n" % len(groups(data["coordinator_finalize"]))
for g in groups(data["coordinator_finalize"]):
pool = g["pmsg2Pool"]
p1_inner = "\n" + indent(",\n".join(byte_array(m) for m in g["pmsgs1"]), 2) + "\n"
p2_inner = "\n" + indent(",\n".join(byte_array(m) for m in pool), 2) + "\n"
s += indent("{\n%s,\n{%s},\n%s,\n%s, {%s} },\n" % (
indent(init_params(g["params"]) + ",", 1)[:-1],
p1_inner,
indent(byte_array(g["cmsg1"]) + ",", 1)[:-1],
len(pool),
p2_inner,
))
s += "};\n"
coord_finalize_cases = []
for gi, g in enumerate(groups(data["coordinator_finalize"])):
for c in g.get("validTestCases", []):
coord_finalize_cases.append((gi, 1, c))
for c in g.get("errorTestCases", []):
if map_error(c, "coordinator_finalize") is None:
continue
coord_finalize_cases.append((gi, 0, c))
s += "static const struct chilldkg_vec_coord_finalize_case chilldkg_vec_coord_finalize_cases[%d] = {\n" % len(coord_finalize_cases)
for gi, is_valid, c in coord_finalize_cases:
if is_valid:
error_code, fault_index = "CHILLDKG_VEC_OK", "UINT32_MAX"
expected_cmsg2 = byte_array(c["expectedOutput"]["cmsg2"])
eo = init_dkg_output(c["expectedOutput"]["dkgOutput"])
rec = byte_array(c["expectedOutput"]["recoveryData"])
else:
error_code, fault_index = map_error(c, "coordinator_finalize")
expected_cmsg2 = "{ 0 }"
eo = "{ 0, { 0 }, { 0 }, { { 0 } } }"
rec = "{ 0 }"
s += indent("{ %d, %d, { %s }, %s, %s,\n%s,\n%s,\n%s },\n" % (
is_valid,
len(c["pmsg2Indices"]),
", ".join(map(str, c["pmsg2Indices"])),
error_code, fault_index,
indent(expected_cmsg2 + ",", 1)[:-1],
indent(eo + ",", 1)[:-1],
indent(rec, 1),
))
s += "};\n"
s += "static const size_t chilldkg_vec_coord_finalize_group_case_counts[%d] = {\n" % len(groups(data["coordinator_finalize"]))
for gi in range(len(groups(data["coordinator_finalize"]))):
cnt = sum(1 for x in coord_finalize_cases if x[0] == gi)
s += indent("%d,\n" % cnt)
s += "};\n"
# coordinator_investigate vectors
s += """
struct chilldkg_vec_coord_investigate_group {
struct chilldkg_vec_params params;
unsigned char pmsgs1[CHILLDKG_VEC_MAX_PARTICIPANTS][CHILLDKG_VEC_MAX_PMSG1_LEN];
unsigned char expected_cinv[CHILLDKG_VEC_MAX_PARTICIPANTS][CHILLDKG_VEC_MAX_CINV_LEN];
};
"""
s += "static const struct chilldkg_vec_coord_investigate_group chilldkg_vec_coord_investigate_groups[%d] = {\n" % len(groups(data["coordinator_investigate"]))
for g in groups(data["coordinator_investigate"]):
p1_inner = "\n" + indent(",\n".join(byte_array(m) for m in g["pmsgs1"]), 2) + "\n"
cinv_inner = "\n" + indent(",\n".join(byte_array(m) for m in g["validTestCases"][0]["expectedCinvMsgs"]), 2) + "\n"
s += indent("{\n%s,\n{%s},\n{%s} },\n" % (
indent(init_params(g["params"]) + ",", 1)[:-1],
p1_inner,
cinv_inner,
))
s += "};\n"
# recover vectors
s += """
struct chilldkg_vec_recover_case {
int is_valid;
/* Coordinator recovery if has_hostseckey == 0. */
int has_hostseckey;
unsigned char hostseckey[32];
size_t recovery_len;
unsigned char recovery[CHILLDKG_VEC_MAX_RECOVERY_LEN];
/* Expected result: CHILLDKG_VEC_OK or CHILLDKG_VEC_INVALID_INPUT. */
int expected_error;
struct chilldkg_vec_dkg_output expected_output;
struct chilldkg_vec_params expected_params;
};
"""
recover_cases = []
for c in data["recover"]["validTestCases"]:
recover_cases.append((1, c))
for c in data["recover"]["errorTestCases"]:
if map_error(c, "recover") is None:
continue
recover_cases.append((0, c))
s += "static const struct chilldkg_vec_recover_case chilldkg_vec_recover_cases[%d] = {\n" % len(recover_cases)
for is_valid, c in recover_cases:
if is_valid:
error_code = "CHILLDKG_VEC_OK"
eo = init_dkg_output(c["expectedOutput"]["dkgOutput"])
ep = init_params(c["expectedOutput"]["params"])
else:
error_code, _ = map_error(c, "recover")
eo = "{ 0, { 0 }, { 0 }, { { 0 } } }"
ep = "{ 0, 0, { { 0 } } }"
s += indent("{ %d, %d, %s, %d,\n%s,\n%s,\n%s,\n%s },\n" % (
is_valid,
1 if c["hostseckey"] else 0,
byte_array(c["hostseckey"] or "00" * 32),
len(bytes.fromhex(c["recoveryData"])),
indent(byte_array(c["recoveryData"]) + ",", 1)[:-1],
error_code,
indent(eo + ",", 1)[:-1],
indent(ep, 1),
))
s += "};\n"
if skipped_cases:
s += "\n/* Skipped test cases (not expressible in the C API):\n"
for case in skipped_cases:
s += " * - %s\n" % case
s += " */\n"
s += "\n#endif\n"
print(s)