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.
288 lines
13 KiB
Python
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()
|