mirror of
https://github.com/bitcoin/bips.git
synced 2026-11-09 19:47:16 +00:00
315 lines
11 KiB
Python
315 lines
11 KiB
Python
#!/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()
|