#!/usr/bin/env python3 # Test harness for the BabyBear field + degree-4 extension sub-component (port spec section 2, the # FOUNDATION component (a)). Runs: # 1. F_p field axioms over many random cases (comm/assoc/distrib/identities/inverses), # 2. inverse cross-check: Fermat a^(p-2) == an INDEPENDENT extended-Euclid bignum inverse # (Python pow(a, -1, p)), for many a; and the (p-1) == -1 identities, # 3. exponent / Frobenius consistency: a^(p-1) == 1, a^p == a, pow == naive repeated mul, # 4. constants (proven offline, NO external fetch): generator has order exactly p-1; the two-adic # generator of the order-2^k subgroup has order exactly 2^k; W is a quadratic non-residue and # p == 1 mod 4, so x^4 - W is irreducible and F_{p^4} is a real field; x^4 == W in F_{p^4}, # 5. F_{p^4} axioms: ring laws, base embedding is a homomorphism, a * inv(a) == 1 for many a # (including non-base elements), Frobenius^4 == id and Frobenius fixes the base field, # 6. cross-language agreement: Python vs Node vs Java produce byte-identical results on a shared # vector set (three independent implementations of the same public field definition). # Writes ../results/kat-results-babybear-field.json and exits non-zero on any failure. # # HONEST SCOPE. This validates the field ARITHMETIC against the public BabyBear definition and an # independent bignum reference. It is the bottom layer of a six-component STARK port; it does NOT # verify any real STARK proof, and the top-level 0x0AE8 precompile stays fail-closed (returns EMPTY # for every input). GENERATOR = 31 and W = 11 are proven here to be a real generator and a real # non-residue; that they are Plonky3's EXACT chosen constants is [VERIFY]/[MEASURE] against # p3-baby-bear at the pinned SP1 v6.1.0 revision (see docs/AERE-STARK-VERIFIER-PORT-SPEC.md sec 2). import json import os import random import subprocess import sys from shutil import which HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HERE) import babybear_field_reference as F # noqa: E402 RESULTS = os.path.normpath(os.path.join(HERE, "..", "results", "kat-results-babybear-field.json")) _pass = 0 _fail = 0 _notes = [] def check(name, cond): global _pass, _fail if cond: _pass += 1 else: _fail += 1 print(f"FAIL {name}") # ---- 1. F_p field axioms + 2. inverse cross-check + 3. exponent/Frobenius -------------------------- def base_field_axioms(): p = F.P rnd = random.Random(0xBEEF) for _ in range(20000): a = rnd.randrange(p) b = rnd.randrange(p) c = rnd.randrange(p) check("add.comm", F.add(a, b) == F.add(b, a)) check("mul.comm", F.mul(a, b) == F.mul(b, a)) check("add.assoc", F.add(F.add(a, b), c) == F.add(a, F.add(b, c))) check("mul.assoc", F.mul(F.mul(a, b), c) == F.mul(a, F.mul(b, c))) check("distrib", F.mul(a, F.add(b, c)) == F.add(F.mul(a, b), F.mul(a, c))) check("add.id", F.add(a, 0) == a) check("mul.id", F.mul(a, 1) == a) check("sub.def", F.add(F.sub(a, b), b) == a) check("neg.def", F.add(a, F.neg(a)) == 0) check("mul.vs.bignum", F.mul(a, b) == (a * b) % p) # independent bignum cross-check if a != 0: # Fermat inverse vs INDEPENDENT extended-Euclid inverse (Python pow(a, -1, p)). check("inv.fermat.vs.euclid", F.inv(a) == pow(a, -1, p)) check("inv.def", F.mul(a, F.inv(a)) == 1) check("fermat.little", F.pow_(a, p - 1) == 1) check("frobenius.base", F.pow_(a, p) == a) # a^p == a on the base field # (p-1) behaves exactly as -1 check("minus1.is.neg1", F.P - 1 == F.neg(1)) check("minus1.sq", F.mul(F.P - 1, F.P - 1) == 1) check("minus1.add", F.add(F.P - 1, 1) == 0) check("inv.zero.convention", F.inv(0) == 0) # pow vs naive repeated multiplication for small exponents for a in (2, 3, 31, 1000, F.P - 1): acc = 1 for e in range(0, 40): check(f"pow.naive.a{a}.e{e}", F.pow_(a, e) == acc) acc = F.mul(acc, a) # ---- 4. constants: generator, two-adic subgroup, W / irreducibility ------------------------------- def constants_offline_proofs(): p = F.P check("p.value", p == 2**31 - 2**27 + 1 == 15 * 2**27 + 1 == 0x78000001) check("p.minus1.factor", (p - 1) == (2**27) * 3 * 5) check("p.mod4", p % 4 == 1) # generator has order exactly p-1 (real proof via full factorization) check("generator.order", F.multiplicative_order(F.GENERATOR) == p - 1) _notes.append(f"multiplicative generator {F.GENERATOR} proven order = p-1 = {p-1} [VERIFY it is Plonky3's]") # two-adic generator of order-2^k subgroup has order exactly 2^k, for k = 1..27 tag = F.two_adic_generator(27) check("twoadic27.order", F.pow_(tag, 2**27) == 1 and F.pow_(tag, 2**26) != 1) for k in range(1, 28): g = F.two_adic_generator(k) check(f"twoadic.order.k{k}", F.pow_(g, 2**k) == 1 and (k == 0 or F.pow_(g, 2 ** (k - 1)) != 1)) _notes.append(f"two_adic_generator(27) = {tag}, order proven 2^27 [VERIFY exact Plonky3 value]") # W = 11: quadratic non-residue + p == 1 mod 4 => x^4 - W irreducible => F_{p^4} is a field check("W.qnr", F.is_quadratic_non_residue(F.W)) _notes.append( f"W = {F.W}: 11^((p-1)/2) == p-1 (QNR) and p == 1 mod 4 => x^4 - {F.W} irreducible " f"(Lidl-Niederreiter Thm 3.75) => F_(p^4) is a genuine field [VERIFY it is Plonky3's W]" ) # defining relation x^4 == W in the extension x = [0, 1, 0, 0] check("x4.eq.W", F.ext_eq(F.ext_pow(x, 4), F.ext_from_base(F.W))) # order of x = 4 * order(W); must divide 4*(p-1) ordW = F.multiplicative_order(F.W) ordx = 4 * ordW check("ordx.divides", (4 * (p - 1)) % ordx == 0) _notes.append(f"ord(W=11) = {ordW} = 2^27 * 5; ord(x) = 4*ord(W) = {ordx} divides 4(p-1)") # ---- 5. F_{p^4} extension axioms ------------------------------------------------------------------- def extension_axioms(): rnd = random.Random(0xF00D) one = [1, 0, 0, 0] zero = [0, 0, 0, 0] for _ in range(10000): a = [rnd.randrange(F.P) for _ in range(4)] b = [rnd.randrange(F.P) for _ in range(4)] c = [rnd.randrange(F.P) for _ in range(4)] check("ext.add.comm", F.ext_eq(F.ext_add(a, b), F.ext_add(b, a))) check("ext.mul.comm", F.ext_eq(F.ext_mul(a, b), F.ext_mul(b, a))) check("ext.add.assoc", F.ext_eq(F.ext_add(F.ext_add(a, b), c), F.ext_add(a, F.ext_add(b, c)))) check("ext.mul.assoc", F.ext_eq(F.ext_mul(F.ext_mul(a, b), c), F.ext_mul(a, F.ext_mul(b, c)))) check("ext.distrib", F.ext_eq(F.ext_mul(a, F.ext_add(b, c)), F.ext_add(F.ext_mul(a, b), F.ext_mul(a, c)))) check("ext.mul.id", F.ext_eq(F.ext_mul(a, one), a)) check("ext.sub.def", F.ext_eq(F.ext_add(F.ext_sub(a, b), b), a)) check("ext.neg.def", F.ext_eq(F.ext_add(a, F.ext_neg(a)), zero)) if not F.ext_eq(a, zero): check("ext.inv.def", F.ext_is_one(F.ext_mul(a, F.ext_inv(a)))) # Frobenius^4 == id; Frobenius fixes the base field; Frobenius is a ring homomorphism f = a for _ in range(4): f = F.ext_frobenius(f) check("ext.frob4.id", F.ext_eq(f, a)) check("ext.mul.vs.frob", F.ext_eq(F.ext_frobenius(F.ext_mul(a, b)), F.ext_mul(F.ext_frobenius(a), F.ext_frobenius(b)))) # base embedding is a ring homomorphism, and is fixed by Frobenius for _ in range(2000): u = rnd.randrange(F.P) v = rnd.randrange(F.P) check("embed.add", F.ext_eq(F.ext_add(F.ext_from_base(u), F.ext_from_base(v)), F.ext_from_base(F.add(u, v)))) check("embed.mul", F.ext_eq(F.ext_mul(F.ext_from_base(u), F.ext_from_base(v)), F.ext_from_base(F.mul(u, v)))) check("embed.frob.fix", F.ext_eq(F.ext_frobenius(F.ext_from_base(u)), F.ext_from_base(u))) check("ext.inv.zero.convention", F.ext_eq(F.ext_inv([0, 0, 0, 0]), [0, 0, 0, 0])) # ---- 6. cross-language agreement (Python vs Node vs Java) ----------------------------------------- def canonicalize(v): # Structural comparison ignoring key order / whitespace / int-vs-float JSON encoding. def i(x): return int(x) def ia(x): return [int(e) for e in x] return { "constants": {k: i(x) for k, x in v["constants"].items()}, "base_unary": [(i(r["a"]), i(r["neg"]), i(r["inv"]), i(r["pow"])) for r in v["base_unary"]], "base_binary": [(i(r["a"]), i(r["b"]), i(r["add"]), i(r["sub"]), i(r["mul"])) for r in v["base_binary"]], "ext_unary": [(ia(r["a"]), ia(r["neg"]), ia(r["inv"]), ia(r["frob"])) for r in v["ext_unary"]], "ext_binary": [(ia(r["a"]), ia(r["b"]), ia(r["add"]), ia(r["sub"]), ia(r["mul"])) for r in v["ext_binary"]], } def cross_language(): py = canonicalize(F.shared_vectors()) langs = {"python": py} node = which("node") if node: try: out = subprocess.run( [node, os.path.join(HERE, "babybear_field_reference.mjs")], capture_output=True, text=True, timeout=120, check=True, ).stdout.strip() langs["node"] = canonicalize(json.loads(out)) except Exception as e: # noqa: BLE001 _notes.append(f"node cross-check unavailable: {e}") else: _notes.append("node not found on PATH; node cross-check skipped") javac = which("javac") java = which("java") if javac and java: try: subprocess.run( [javac, "-d", os.path.join(HERE, "out"), os.path.join(HERE, "BabyBearFieldSelfTest.java")], capture_output=True, text=True, timeout=180, check=True, ) out = subprocess.run( [java, "-cp", os.path.join(HERE, "out"), "BabyBearFieldSelfTest", "--emit"], capture_output=True, text=True, timeout=120, check=True, ).stdout.strip() langs["java"] = canonicalize(json.loads(out)) except Exception as e: # noqa: BLE001 _notes.append(f"java cross-check unavailable: {e}") else: _notes.append("javac/java not found on PATH; java cross-check skipped") for name, data in langs.items(): if name == "python": continue check(f"crosslang.{name}.agree", data == py) _notes.append("cross-language implementations compared: " + ", ".join(sorted(langs))) return sorted(langs) def main(): base_field_axioms() constants_offline_proofs() extension_axioms() langs = cross_language() report = { "component": "BabyBear field F_p + degree-4 extension F_{p^4} (port spec component (a), FOUNDATION)", "precompile": "0x0000000000000000000000000000000000000ae8", "scope": "port spec section 2; the field-arithmetic foundation the whole STARK verifier is built on", "validates": "F_p and F_{p^4} field axioms; Fermat inverse vs independent extended-Euclid " "bignum; generator has order p-1; two-adic generator order 2^k; W is a QNR so " "x^4-W is irreducible (real field); across Python/Node/Java", "does_not_validate": "any real STARK proof (this is only component (a) of six); and that " "GENERATOR=31 / W=11 / two_adic_generator(27) are Plonky3's EXACT " "constants ([VERIFY]/[MEASURE] vs p3-baby-bear at pinned SP1 v6.1.0)", "top_level_verifier": "unchanged, still fail-closed (returns EMPTY for every input)", "constants": { "P": F.P, "P_expr": "2^31 - 2^27 + 1 = 15*2^27 + 1 = 0x78000001", "generator": F.GENERATOR, "two_adicity": F.TWO_ADICITY, "two_adic_generator_27": F.two_adic_generator(27), "W": F.W, }, "cross_language": langs, "flags": [ "[VERIFY] GENERATOR = 31 is Plonky3 p3-baby-bear's chosen multiplicative generator " "(order = p-1 is PROVEN here; the identity as Plonky3's is [VERIFY])", "[VERIFY] W = 11 is Plonky3's BabyBear BinomialExtensionField<_,4> non-residue " "(x^4-11 irreducibility is PROVEN here; the identity as Plonky3's W is [VERIFY]/[MEASURE])", "[VERIFY] two_adic_generator(27) exact value vs Plonky3 (order 2^27 is PROVEN here)", "[MEASURE] byte-for-byte conformance of Fp/Fp4 outputs vs p3-baby-bear / p3-field unit " "vectors at the pinned revision (needs the pinned Plonky3 source)", ], "notes": _notes, "passed": _pass, "failed": _fail, "total": _pass + _fail, } os.makedirs(os.path.dirname(RESULTS), exist_ok=True) with open(RESULTS, "w") as f: json.dump(report, f, indent=2) print("\n=== BabyBear field + F_{p^4} KAT ===") print(f"cross-language: {', '.join(langs)}") for nnote in _notes: print(f"note: {nnote}") print(f"PASS={_pass} FAIL={_fail} TOTAL={_pass + _fail}") print(f"results -> {RESULTS}") if _fail != 0: print("BABYBEAR_FIELD_KAT_FAILED") sys.exit(1) print("BABYBEAR_FIELD_KAT_OK") if __name__ == "__main__": main()