aere-research/pq-stark/stark_e2e_reference.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

259 lines
12 KiB
Python

#!/usr/bin/env python3
# ASSEMBLED END-TO-END BabyBear + FRI STARK verifier for the PQ STARK-verify precompile 0x0AE8 reference.
#
# ============================ WHAT THIS IS (read first) ============================
# The six generic components of a BabyBear+FRI STARK verifier were each conformance-confirmed INDIVIDUALLY
# against the pinned Plonky3 0.4.3-succinct crates (field, Poseidon2, MMCS, FRI verify_query, duplex
# challenger, generic AIR quotient check). THIS module wires all six together into ONE full p3-uni-stark
# verify pipeline and runs it, end to end, against a REAL proof emitted by the pinned p3-uni-stark PROVER
# (e2e-extractor -> e2e_ground_truth.json), which the pinned p3-uni-stark VERIFIER accepts (the ground
# truth). It asserts the assembled pipeline ACCEPTS the genuine proof and REJECTS tampered ones (a trace
# opening, a FRI layer, an input-batch opening, the public values, the final poly).
#
# The pipeline follows p3-uni-stark verifier.rs + p3-fri two_adic_pcs.rs::verify + p3-fri verifier.rs
# VERBATIM (pinned 0.4.3-succinct):
# 1. observe(trace commit); alpha_c = sample_ext (constraint challenge)
# 2. observe(quotient commit); zeta = sample_ext; zeta_next = zeta * g_trace
# 3. PCS.verify:
# alpha_fri = sample_ext (FRI batch-combining challenge)
# verify_shape_and_sample_challenges: per commit-phase commit observe+sample beta; observe final
# poly; check grinding PoW; sample num_queries query indices
# per query: reduced-opening combination sum_i alpha_fri^i (p(x)-p(z))/(x-z) over the input-batch
# MMCS openings (input-batch root recomputed by the CONFIRMED MMCS verify_batch), then
# verify_query (the CONFIRMED FRI fold + commit-phase MMCS opening) and folded == final_poly
# 4. AIR quotient consistency: folded_constraints(zeta) * inv_zeroifier == quotient(zeta)
#
# HONEST SCOPE. The end-to-end proof verified here is a p3-uni-stark EXAMPLE AIR (Fibonacci / degree-3
# multiply) under an EXAMPLE FRI config (log_blowup=2, num_queries=28, pow_bits=8), NOT an Aere production
# circuit and NOT an SP1 6.1.0 proof. Aere's own zk-circuits are SP1 guest programs, and SP1 6.1.0 is
# Hypercube (KoalaBear multilinear: BaseFold/Jagged + sumcheck-zerocheck + LogUp-GKR), which this
# BabyBear+FRI pipeline does not target. This module does NOT run in the precompile and does NOT make
# 0x0AE8 accept anything: the top level stays FAIL-CLOSED (SP1_RECURSION_AIR_PORTED=false). See the port
# spec and README for exactly what an Aere-own / SP1 end-to-end KAT still needs.
import json
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import babybear_field_reference as F # (a) F_p and F_{p^4} CONFIRMED
import poseidon2_babybear_reference as P2 # (b) Poseidon2 permutation CONFIRMED
import mmcs_babybear_reference as M # (c) FieldMerkleTreeMmcs verify_batch CONFIRMED
import challenger_reference as CH # (f) DuplexChallenger + grinding CONFIRMED
import fri_verify_reference as FRI # (d) FRI verify_query (fold + opening) CONFIRMED
import air_quotient_reference as AQ # (e) generic AIR quotient-consistency CONFIRMED
GROUND_TRUTH = os.path.join(HERE, "e2e_ground_truth.json")
P = F.P
def _log2_exact(n):
b = n.bit_length() - 1
assert (1 << b) == n, f"{n} is not a power of two"
return b
def verify_stark(case, tamper=None):
"""Full assembled BabyBear+FRI STARK verify of a real p3-uni-stark proof. Returns True iff every
stage passes. `tamper` in {None, 'trace_open', 'fri_layer', 'input_batch', 'public_values',
'final_poly'} injects a single fault to demonstrate rejection."""
c = json.loads(json.dumps(case)) # deep copy so tampers do not mutate the shared ground truth
log_blowup = c["log_blowup"]
num_queries = c["num_queries"]
pow_bits = c["pow_bits"]
degree_bits = c["degree_bits"]
air = c["air"]
pis = list(c["public_values"])
trace_commit = list(c["commitments"]["trace"])
quotient_commit = list(c["commitments"]["quotient_chunks"])
trace_local = [list(v) for v in c["opened_values"]["trace_local"]]
trace_next = [list(v) for v in c["opened_values"]["trace_next"]]
quotient_chunks = [[list(v) for v in ch] for ch in c["opened_values"]["quotient_chunks"]]
fp = c["opening_proof"]["fri_proof"]
commit_phase_commits = [list(x) for x in fp["commit_phase_commits"]]
final_poly = list(fp["final_poly"])
pow_witness = fp["pow_witness"]
query_proofs = fp["query_proofs"]
query_openings = c["opening_proof"]["query_openings"]
# ---- tampers that live in the claimed openings / public inputs ----
if tamper == "public_values":
pis[-1] = (pis[-1] + 1) % P
if tamper == "trace_open":
trace_local[0][0] = (trace_local[0][0] + 1) % P
if tamper == "final_poly":
final_poly[0] = (final_poly[0] + 1) % P
quotient_degree = len(quotient_chunks)
log_max_height = len(commit_phase_commits) + log_blowup # = log_global_max_height
# ---- 1) transcript: observe trace, sample constraint alpha ----
ch = CH.DuplexChallenger()
ch.observe_slice(trace_commit)
alpha_c = ch.sample_ext()
# ---- 2) observe quotient, sample DEEP point zeta ----
ch.observe_slice(quotient_commit)
zeta = ch.sample_ext()
g_trace = F.two_adic_generator(degree_bits)
zeta_next = F.ext_mul(zeta, F.ext_from_base(g_trace))
# ---- 3) PCS.verify ----
alpha_fri = ch.sample_ext() # batch-combining challenge (distinct from the constraint alpha)
# verify_shape_and_sample_challenges (p3-fri verifier.rs)
betas = []
for comm in commit_phase_commits:
ch.observe_slice(comm)
betas.append(ch.sample_ext())
ch.observe_ext(final_poly)
if len(query_proofs) != num_queries:
return False
if not ch.check_witness(pow_bits, pow_witness):
return False
query_indices = [ch.sample_bits(log_max_height) for _ in range(num_queries)]
# rounds structure (p3-uni-stark verifier.rs pcs.verify call):
# batch 0 = trace : 1 matrix of size 2^degree_bits, opened at [(zeta, trace_local), (zeta_next, trace_next)]
# batch 1 = quotient : quotient_degree matrices of size 2^degree_bits, opened at [(zeta, chunk_i)]
trace_size = 1 << degree_bits
rounds = [
(trace_commit, [(trace_size, [(zeta, trace_local), (zeta_next, trace_next)])]),
(quotient_commit, [(trace_size, [(zeta, quotient_chunks[i])]) for i in range(quotient_degree)]),
]
# verify_challenges: for each query, build the reduced openings, then FRI verify_query.
for qi in range(num_queries):
index = query_indices[qi]
per_query_batches = query_openings[qi]
if len(per_query_batches) != len(rounds):
return False
# ro / alpha_pow indexed by log_height (p3-fri two_adic_pcs.rs::verify)
ro = [[0, 0, 0, 0] for _ in range(log_max_height + 2)]
alpha_pow = [F.ext_from_base(1) for _ in range(log_max_height + 2)]
for bi, (batch_commit, mats) in enumerate(rounds):
batch = per_query_batches[bi]
opened_values = [list(r) for r in batch["opened_values"]] # per matrix: base LDE row
opening_proof = [list(d) for d in batch["opening_proof"]] # sibling digest path
if tamper == "input_batch" and qi == 0 and bi == 0:
opened_values[0][0] = (opened_values[0][0] + 1) % P
batch_heights = [ms << log_blowup for (ms, _) in mats]
batch_dims = [(0, h) for h in batch_heights] # MMCS ignores width; heights drive the tree
log_batch_max_height = _log2_exact(max(batch_heights))
bits_reduced = log_max_height - log_batch_max_height
reduced_index = index >> bits_reduced
# input-batch MMCS opening (component (c)): recompute the batch root, compare to batch_commit.
if not M.verify_batch(batch_commit, batch_dims, reduced_index, opened_values, opening_proof):
return False
for mat_opening, (ms, points) in zip(opened_values, mats):
log_height = _log2_exact(ms) + log_blowup
br2 = log_max_height - log_height
rev = FRI.reverse_bits_len(index >> br2, log_height)
x = F.mul(F.GENERATOR, F.pow_(F.two_adic_generator(log_height), rev)) # LDE coset point (base)
x_ext = F.ext_from_base(x)
for z, ps_at_z in points:
if len(mat_opening) != len(ps_at_z):
return False
for p_at_x, p_at_z in zip(mat_opening, ps_at_z):
# quotient = (p_at_x - p_at_z) / (x - z), in F_{p^4}
num = F.ext_sub(F.ext_from_base(p_at_x), list(p_at_z))
den = F.ext_sub(x_ext, z)
q = F.ext_mul(num, F.ext_inv(den))
ro[log_height] = F.ext_add(ro[log_height], F.ext_mul(alpha_pow[log_height], q))
alpha_pow[log_height] = F.ext_mul(alpha_pow[log_height], alpha_fri)
# FRI verify_query over the CONFIRMED fold + commit-phase MMCS opening (components (d),(c)).
layers = []
for st in query_proofs[qi]["commit_phase_openings"]:
layers.append({
"sibling_value": list(st["sibling_value"]),
"opening_proof": [list(d) for d in st["opening_proof"]],
})
if tamper == "fri_layer" and qi == 0:
layers[0]["sibling_value"][0] = (layers[0]["sibling_value"][0] + 1) % P
folded = FRI.verify_query(log_blowup, log_max_height, commit_phase_commits, betas,
index, ro, layers)
if folded is None:
return False
if folded != final_poly:
return False
# ---- 4) AIR quotient consistency (component (e)) with the constraint alpha + zeta ----
is_first, is_last, is_transition, inv_zeroifier = AQ.selectors_at_point(degree_bits, zeta)
quotient = AQ.reconstruct_quotient(degree_bits, quotient_chunks, zeta)
folder = AQ.AIR_FOLDERS[air]
folded_constraints = folder(trace_local, trace_next, pis,
is_first, is_last, is_transition, alpha_c)
if not F.ext_eq(F.ext_mul(folded_constraints, inv_zeroifier), quotient):
return False
return True
TAMPERS = ("trace_open", "fri_layer", "input_batch", "public_values", "final_poly")
def conformance_kats():
"""(passed, total, details). (0) tie ground truth to the CONFIRMED Poseidon2 permutation + Val
generator; (1) ACCEPT each genuine library-accepted proof end to end; (2) REJECT each tamper."""
with open(GROUND_TRUTH) as fp:
gt = json.load(fp)
passed = 0
total = 0
details = []
def check(name, cond):
nonlocal passed, total
total += 1
if cond:
passed += 1
details.append({"check": name, "pass": bool(cond)})
# (0) sanity: same permutation + generator the CONFIRMED components use.
check("perm_zeros_matches_poseidon2", P2.permute([0] * 16) == gt["perm_zeros"])
check("val_generator_is_31", gt["val_generator"] == F.GENERATOR == 31)
for case in gt["cases"]:
name = case["name"]
check(f"{name}.library_accept", case["library_accept"] is True)
# (1) genuine proof accepts end to end.
check(f"{name}.e2e_accept", verify_stark(case) is True)
# (2) each tamper is rejected.
for t in TAMPERS:
check(f"{name}.reject[{t}]", verify_stark(case, t) is False)
return passed, total, details
if __name__ == "__main__":
if len(sys.argv) > 1 and sys.argv[1] == "kat":
p, t, det = conformance_kats()
for d in det:
print(("PASS" if d["pass"] else "FAIL"), d["check"])
print(f"conformance: PASS={p} FAIL={t - p} TOTAL={t}")
sys.exit(0 if p == t else 1)
# default: emit the accept/reject matrix as JSON
with open(GROUND_TRUTH) as fp:
gt = json.load(fp)
out = {"cases": []}
for case in gt["cases"]:
out["cases"].append({
"name": case["name"],
"accept": verify_stark(case),
"rejects": {t: (not verify_stark(case, t)) for t in TAMPERS},
})
json.dump(out, sys.stdout, indent=2)