#!/usr/bin/env python3 """ Run test vectors for BIP 459 DahLIAS fully aggregated signatures for secp256k1. Usage: python run_test_vectors.py Expects CSV files generated by gen_test_vectors.py in vectors/ directory. """ import csv import os import sys from pathlib import Path from typing import List, Tuple sys.path.insert(0, str(Path(__file__).parent / "secp256k1lab/src")) from secp256k1lab.secp256k1 import G, GE, Scalar from reference import NonceGenInternal, NonceAgg, Sign, SigAgg, Verify, PartialSigVerify, TweakSK, TweakPK VECTORS_DIR = Path(__file__).resolve().parent / "vectors" NONCE_GEN_VECTORS = VECTORS_DIR / "test_vectors_nonce_gen.csv" NONCE_AGG_VECTORS = VECTORS_DIR / "test_vectors_nonce_agg.csv" SIGN_VECTORS = VECTORS_DIR / "test_vectors_sign.csv" SIGN_ERROR_VECTORS = VECTORS_DIR / "test_vectors_sign_error.csv" VERIFY_VECTORS = VECTORS_DIR / "test_vectors_verify.csv" PARTIAL_SIG_VERIFY_VECTORS = VECTORS_DIR / "test_vectors_partial_sig_verify.csv" TWEAK_VECTORS = VECTORS_DIR / "test_vectors_tweak.csv" def parse_hex_list(hex_str: str) -> List[bytes]: if not hex_str: return [] return [bytes.fromhex(h) for h in hex_str.split(";")] def parse_pubnonce(b: bytes) -> Tuple[GE, GE]: if len(b) != 66: raise ValueError("pubnonce must be 66 bytes") return GE.from_bytes_compressed(b[:33]), GE.from_bytes_compressed(b[33:]) def parse_secnonce(b: bytes) -> Tuple[Scalar, Scalar]: if len(b) != 64: raise ValueError("secnonce must be 64 bytes") return Scalar.from_bytes_checked(b[:32]), Scalar.from_bytes_checked(b[32:]) def parse_sig(b: bytes) -> Tuple[GE, Scalar]: """Parse R (32-byte x-only) || s (32-byte big-endian). Lifts R to even y and raises ValueError on length mismatch, off-curve R, or s >= n. """ if len(b) != 64: raise ValueError("signature must be 64 bytes") R = GE.from_bytes_xonly(b[:32]) s = Scalar.from_bytes_checked(b[32:]) return R, s def run_nonce_gen_tests() -> List[str]: failures = [] with open(NONCE_GEN_VECTORS, newline="", encoding="utf-8") as f: reader = csv.reader(f) next(reader) for row in reader: (index, rand_str, sk_str, extra_in_str, expected_secnonce_str, expected_pubnonce_str, comment) = row sk = Scalar.from_bytes_checked(bytes.fromhex(sk_str)) if sk_str else None (r1, r2), (R1, R2) = NonceGenInternal(bytes.fromhex(rand_str), sk, bytes.fromhex(extra_in_str)) secnonce = r1.to_bytes() + r2.to_bytes() pubnonce = R1.to_bytes_compressed() + R2.to_bytes_compressed() if (secnonce.hex().upper() != expected_secnonce_str or pubnonce.hex().upper() != expected_pubnonce_str): failures.append(f"NonceGen #{index}: {comment}") return failures def run_nonce_agg_tests() -> List[str]: failures = [] with open(NONCE_AGG_VECTORS, newline="", encoding="utf-8") as f: reader = csv.reader(f) next(reader) for row in reader: index, pubnonces_str, expected_result, expected_aggnonce_str, comment = row expected = (expected_result == "TRUE") try: pubnonces = [parse_pubnonce(b) for b in parse_hex_list(pubnonces_str)] R1, R2 = NonceAgg(pubnonces) aggnonce = R1.to_bytes_compressed() + R2.to_bytes_compressed() except ValueError: aggnonce = None if expected: ok = aggnonce is not None and aggnonce.hex().upper() == expected_aggnonce_str else: ok = aggnonce is None if not ok: failures.append(f"NonceAgg #{index}: {comment}") return failures def run_sign_tests() -> List[str]: failures = [] with open(SIGN_VECTORS, newline="", encoding="utf-8") as f: reader = csv.reader(f) next(reader) for row in reader: (index, sks_str, msgs_str, secnonces_str, expected_aggnonce_str, expected_psigs_str, expected_sig_str, comment) = row try: sks = [Scalar.from_bytes_checked(b) for b in parse_hex_list(sks_str)] msgs = parse_hex_list(msgs_str) secnonces = [parse_secnonce(b) for b in parse_hex_list(secnonces_str)] expected_aggnonce = bytes.fromhex(expected_aggnonce_str) expected_psigs = parse_hex_list(expected_psigs_str) expected_sig = bytes.fromhex(expected_sig_str) pks = [GE.from_bytes_xonly((sk * G).to_bytes_xonly()) for sk in sks] pubnonces = [(r1 * G, r2 * G) for (r1, r2) in secnonces] aggnonce = NonceAgg(pubnonces) aggnonce_bytes = (aggnonce[0].to_bytes_compressed() + aggnonce[1].to_bytes_compressed()) if aggnonce_bytes != expected_aggnonce: failures.append(f"Sign #{index}: {comment} (aggnonce mismatch)") continue psigs = [ Sign(secnonces[i], sks[i], msgs[i], aggnonce, pks, msgs, pubnonces) for i in range(len(sks)) ] derived_psigs = [p.to_bytes() for p in psigs] if derived_psigs != expected_psigs: failures.append(f"Sign #{index}: {comment} (partial sig mismatch)") continue for i in range(len(sks)): if not PartialSigVerify(psigs[i], pks, msgs, pubnonces, i): failures.append(f"Sign #{index}: {comment} (PartialSigVerify failed for signer {i})") break else: R, s = SigAgg(aggnonce, pks, msgs, pubnonces, psigs) actual_sig = R.to_bytes_xonly() + s.to_bytes() if actual_sig != expected_sig: failures.append(f"Sign #{index}: {comment} (aggsig mismatch)") elif not Verify(pks, msgs, (R, s)): failures.append(f"Sign #{index}: {comment} (Verify failed)") except ValueError as e: failures.append(f"Sign #{index}: {comment} (raised {e!r})") return failures def run_sign_error_tests() -> List[str]: failures = [] with open(SIGN_ERROR_VECTORS, newline="", encoding="utf-8") as f: reader = csv.reader(f) next(reader) for row in reader: (index, sk_str, msg_str, secnonce_str, aggnonce_str, pks_str, msgs_str, pubnonces_str, comment) = row sk = Scalar.from_bytes_checked(bytes.fromhex(sk_str)) m = bytes.fromhex(msg_str) secnonce = parse_secnonce(bytes.fromhex(secnonce_str)) pks = [GE.from_bytes_xonly(b) for b in parse_hex_list(pks_str)] msgs = parse_hex_list(msgs_str) pubnonces = [parse_pubnonce(b) for b in parse_hex_list(pubnonces_str)] try: aggnonce = parse_pubnonce(bytes.fromhex(aggnonce_str)) Sign(secnonce, sk, m, aggnonce, pks, msgs, pubnonces) except ValueError: continue failures.append(f"SignError #{index}: {comment} (Sign did not fail)") return failures def run_partial_sig_verify_tests() -> List[str]: failures = [] with open(PARTIAL_SIG_VERIFY_VECTORS, newline="", encoding="utf-8") as f: reader = csv.reader(f) next(reader) for row in reader: (index, signer_index_str, psig_str, pks_str, msgs_str, pubnonces_str, expected_result, comment) = row expected = (expected_result == "TRUE") try: signer_index = int(signer_index_str) psig = Scalar.from_bytes_checked(bytes.fromhex(psig_str)) pks = [GE.from_bytes_xonly(b) for b in parse_hex_list(pks_str)] msgs = parse_hex_list(msgs_str) pubnonces = [parse_pubnonce(b) for b in parse_hex_list(pubnonces_str)] result = PartialSigVerify(psig, pks, msgs, pubnonces, signer_index) except ValueError: result = False if result != expected: failures.append(f"PartialSigVerify #{index}: {comment}") return failures def run_verify_tests() -> List[str]: failures = [] with open(VERIFY_VECTORS, newline="", encoding="utf-8") as f: reader = csv.reader(f) next(reader) for row in reader: index, pks_str, msgs_str, sig_str, expected_result, comment = row expected = (expected_result == "TRUE") try: pks = [GE.from_bytes_xonly(b) for b in parse_hex_list(pks_str)] msgs = parse_hex_list(msgs_str) R, s = parse_sig(bytes.fromhex(sig_str)) result = Verify(pks, msgs, (R, s)) except ValueError: result = False if result != expected: failures.append(f"Verify #{index}: {comment}") return failures def run_tweak_tests() -> List[str]: failures = [] with open(TWEAK_VECTORS, newline="", encoding="utf-8") as f: reader = csv.reader(f) next(reader) for row in reader: (index, sk_str, t_str, is_xonly_str, expected_result, exp_sk_str, exp_pk_str, comment) = row sk = Scalar.from_bytes_checked(bytes.fromhex(sk_str)) is_xonly = (is_xonly_str == "TRUE") expected = (expected_result == "TRUE") results = [] for tweak_fn, key in ((TweakSK, sk), (TweakPK, sk * G)): try: t = Scalar.from_bytes_checked(bytes.fromhex(t_str)) results.append(tweak_fn(key, t, is_xonly)) except ValueError: results.append(None) sk_out, pk_out = results if expected: ok = (sk_out is not None and pk_out is not None and sk_out.to_bytes().hex().upper() == exp_sk_str and pk_out.to_bytes_compressed().hex().upper() == exp_pk_str) else: ok = sk_out is None and pk_out is None if not ok: failures.append(f"Tweak #{index}: {comment}") return failures def main(): for filename in [NONCE_GEN_VECTORS, NONCE_AGG_VECTORS, SIGN_VECTORS, SIGN_ERROR_VECTORS, VERIFY_VECTORS, PARTIAL_SIG_VERIFY_VECTORS, TWEAK_VECTORS]: if not os.path.exists(filename): print(f"ERROR: Missing {filename}. Run gen_test_vectors.py first.") sys.exit(1) failures = [] failures.extend(run_nonce_gen_tests()) failures.extend(run_nonce_agg_tests()) failures.extend(run_sign_tests()) failures.extend(run_sign_error_tests()) failures.extend(run_partial_sig_verify_tests()) failures.extend(run_verify_tests()) failures.extend(run_tweak_tests()) if failures: print("FAILED:") for f in failures: print(f" {f}") sys.exit(1) else: print("All tests have passed.") sys.exit(0) if __name__ == "__main__": main()