aere-research/pq-stark/test_babybear_field.py
Aere Network 4a0b48588c Initial public release
Aere Network public source. Everything here can be checked against the live
chain (chain id 2800, https://rpc.aere.network).

Scope note, stated up front rather than buried: consensus on chain 2800 is
classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at
the signature, precompile, account and transport layers. Nothing here makes the
consensus post-quantum, and no document in it should be read as claiming so.
2026-07-20 01:02:30 +03:00

288 lines
13 KiB
Python

#!/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()