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

246 lines
12 KiB
Python

#!/usr/bin/env python3
# Independent reference for the FRI VERIFIER (the low-degree test at the heart of the STARK),
# component (d) of the PQ STARK-verify precompile 0x0AE8 (direct SP1/Plonky3 inner FRI/STARK verify).
# See docs/AERE-STARK-VERIFIER-PORT-SPEC.md section 5 and spec-fri-babybear.md.
#
# ============================ HONEST SCOPE (read first) ============================
# This module implements the FRI query verification (verify_query / verify_challenges) that Plonky3
# p3-fri performs: for each query, walk the commit-phase folding, checking (1) the MMCS opening of the
# sibling pair at each layer against that layer's commitment (component (c), CONFIRMED), and (2) that
# the arity-2 folding relation holds layer to layer (interpolate the pair through beta at the coset
# point, in F_{p^4}, component (a)), down to a single constant that must equal the committed final
# polynomial. It is CONFIRMED-FROM-SOURCE against the pinned Plonky3 p3-fri 0.4.3-succinct, and a real
# known-answer test PASSES (see conformance() and test_fri_verify.py): the ground-truth extractor
# (pq-stark/fri-extractor) ran the pinned p3-fri PROVER to emit real FRI proofs over known low-degree
# polynomials and confirmed the pinned p3-fri VERIFIER accepts them; this reference reproduces the
# accept and rejects tampered proofs (corrupted opening, wrong fold, non-matching final poly).
#
# WHAT IS AND IS NOT PORTED (the honest boundary):
# - PORTED + CONFIRMED here (component (d)): the FOLD + OPENING relations, i.e. verify_query. It takes
# the transcript-derived betas and query indices (and the reduced openings) as INPUTS.
# - NOT ported (component (f), the duplex-sponge Fiat-Shamir challenger): deriving those betas / query
# indices / grinding IN-CIRCUIT from the transcript. In this KAT the betas and indices come from the
# pinned p3-fri challenger (the ground-truth extractor), supplied as inputs; the transcript BINDING
# that would make them trustless is component (f) and remains un-ported (Challenger.spongePorted =
# false). So even with this passing FRI-relation KAT, the top-level 0x0AE8 stays FAIL-CLOSED.
# - The proof-of-work / grinding check is part of the transcript phase (component (f)); it is not part
# of the fold+opening relation this module verifies.
#
# Source (pinned, checksum-matched to the repo Cargo.lock):
# p3-fri 0.4.3-succinct 5cbc4965ee488f3247867b7ec4bb005b8afa72cb0d461a4dcb1387ecab6426d5
# p3-challenger 0.4.3-succinct b6a908924d43e4cfb93fb41c8346cac211b70314385a9037e9241f5b7f3eaf77
# p3-dft 0.4.3-succinct be6408b10a2c27eb13a7d5580c546c2179a8dc7dbc10a990657311891f9b41c0
# (commit-phase MMCS: p3-commit ExtensionMmcs over the CONFIRMED p3-merkle-tree ValMmcs, component (c);
# leaf hash/compress: the CONFIRMED Poseidon2-BabyBear permutation, component (b)).
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 # component (a): F_p and F_{p^4} arithmetic (CONFIRMED)
import mmcs_babybear_reference as M # component (c): FieldMerkleTreeMmcs verify_batch (CONFIRMED)
GROUND_TRUTH = os.path.join(HERE, "fri_ground_truth.json")
P = F.P
# ============================ index arithmetic ============================
def reverse_bits_len(x: int, bit_len: int) -> int:
"""Reverse the low `bit_len` bits of x. Mirrors p3-util reverse_bits_len (x.reverse_bits() >>
(usize::BITS - bit_len)). Used to map a query index to its coset point exponent."""
r = 0
for i in range(bit_len):
r = (r << 1) | ((x >> i) & 1)
return r
# ============================ the FRI query verifier ============================
# Elements of F_{p^4} are 4-int lists [c0, c1, c2, c3] (component (a)'s convention, matching Plonky3
# BinomialExtensionField<BabyBear,4>::as_base_slice). A digest is 8 canonical BabyBear ints.
def verify_query(log_blowup, log_max_height, commits, betas, index, ro_full, layers):
"""Reproduce p3-fri verifier::verify_query for one query. Returns the folded evaluation in
F_{p^4} (a 4-int list) on success, or None if any commit-phase MMCS opening fails.
Arguments (all supplied as inputs; the transcript derivation of betas/index is component (f),
un-ported):
log_blowup : FRI log blowup (num_fold_rounds = log_max_height - log_blowup).
log_max_height : log2 of the first-layer (LDE) domain.
commits : the commit-phase MMCS roots, one per fold layer (each 8 BabyBear ints).
betas : the folding challenges, one per layer (each F_{p^4}).
index : this query's index in [0, 2^log_max_height).
ro_full : the reduced openings array indexed by log-height (len >= log_max_height+1),
each an F_{p^4} element (mostly zero; ro_full[h] injected before the fold at
log_folded_height = h-1).
layers : per fold layer, {"sibling_value": F_{p^4}, "opening_proof": [digest,...]}.
"""
folded = [0, 0, 0, 0] # F_{p^4} zero
g = F.two_adic_generator(log_max_height) # base-field two-adic generator
x = F.ext_from_base(F.pow_(g, reverse_bits_len(index, log_max_height))) # coset point, in F_{p^4}
gen1 = F.ext_from_base(F.two_adic_generator(1)) # order-2 root = -1, embedded
idx = index
num_layers = log_max_height - log_blowup
for layer in range(num_layers):
log_folded_height = log_max_height - 1 - layer
# inject the reduced opening at this height (single-codeword LDT: only ro_full[log_max_height])
folded = F.ext_add(folded, ro_full[log_folded_height + 1])
index_sibling = idx ^ 1
index_pair = idx >> 1
# evals = [folded, folded]; the sibling slot is replaced by the opened sibling value.
evals = [list(folded), list(folded)]
evals[index_sibling % 2] = list(layers[layer]["sibling_value"])
# MMCS opening: the pair of F_{p^4} evals flattens to 8 BabyBear coords (ExtensionMmcs), a
# single width-8 row of a height-2^log_folded_height tree, checked by the CONFIRMED base MMCS.
row = evals[0] + evals[1] # 8 base ints
height = 1 << log_folded_height
if not M.verify_batch(commits[layer], [(8, height)], index_pair, [row], layers[layer]["opening_proof"]):
return None # commit-phase MMCS opening failed -> reject
# xs = [x, x]; the sibling's x is negated (x and -x are the arity-2 pair).
xs = [x, F.ext_mul(x, gen1)] if index_sibling % 2 == 1 else [F.ext_mul(x, gen1), x]
# interpolate the pair through beta and evaluate:
# folded = evals[0] + (beta - xs[0]) * (evals[1] - evals[0]) / (xs[1] - xs[0])
beta = betas[layer]
num = F.ext_mul(F.ext_sub(beta, xs[0]), F.ext_sub(evals[1], evals[0]))
den = F.ext_sub(xs[1], xs[0])
folded = F.ext_add(evals[0], F.ext_mul(num, F.ext_inv(den)))
idx = index_pair
x = F.ext_mul(x, x) # x = x^2
return folded
def build_ro_full(log_max_height, ro_top):
"""Construct the reduced-openings array for a single top-codeword LDT: zero everywhere except
ro_full[log_max_height] = ro_top (the codeword value at the query index)."""
ro = [[0, 0, 0, 0] for _ in range(log_max_height + 2)]
ro[log_max_height] = list(ro_top)
return ro
def verify_case(case, tamper=None):
"""Run verify_challenges over every query of a case: accept iff every query's MMCS openings pass
AND every query's folded value equals the committed final polynomial. `tamper` injects a fault to
demonstrate rejection: 'sibling' (corrupt an opened value), 'proof' (corrupt an MMCS sibling
digest), 'beta' (wrong fold challenge), 'final' (non-matching final polynomial)."""
log_blowup = case["log_blowup"]
log_max_height = case["log_max_height"]
commits = case["commit_phase_commits"]
final_poly = list(case["final_poly"])
betas = [list(b) for b in case["betas"]]
if tamper == "final":
final_poly[0] = (final_poly[0] + 1) % P
if tamper == "beta":
betas[0][0] = (betas[0][0] + 1) % P
for q in case["queries"]:
layers = [
{"sibling_value": list(s["sibling_value"]),
"opening_proof": [list(d) for d in s["opening_proof"]]}
for s in q["layers"]
]
if tamper == "sibling":
layers[0]["sibling_value"][0] = (layers[0]["sibling_value"][0] + 1) % P
if tamper == "proof":
layers[0]["opening_proof"][0][0] = (layers[0]["opening_proof"][0][0] + 1) % P
ro_full = build_ro_full(log_max_height, q["ro_top"])
folded = verify_query(log_blowup, log_max_height, commits, betas, q["index"], ro_full, layers)
if folded is None:
return False # a commit-phase MMCS opening failed
if folded != final_poly:
return False # FinalPolyMismatch
return True
# ============================ conformance KAT (real ground truth) ============================
def load_ground_truth():
with open(GROUND_TRUTH) as f:
return json.load(f)
def conformance_kats():
"""Return (passed, total) after: (0) a perm-sanity + two-adic-generator sanity check tying the
ground truth to the CONFIRMED Poseidon2 permutation and component (a)'s generators; (1) accepting
every genuine FRI proof; (2) rejecting each of four tamper variants per case."""
gt = load_ground_truth()
passed = 0
total = 0
# (0) sanity: the extractor's perm and generators match the confirmed sub-components.
total += 1
if M.permute([0] * 16) == gt["perm_zeros"]:
passed += 1
for b in range(28):
total += 1
if F.two_adic_generator(b) == gt["two_adic_generators"][b]:
passed += 1
# (1) accept genuine proofs; (2) reject tampered ones.
for case in gt["cases"]:
total += 1
if verify_case(case):
passed += 1
for tamper in ("sibling", "proof", "beta", "final"):
total += 1
if not verify_case(case, tamper):
passed += 1
return passed, total
# ============================ shared cross-language vector set ============================
# All three languages verify the same ground truth and emit, per case: the accept flag, the four
# tamper-reject flags, and the per-query FOLDED evaluation (F_{p^4}). The harness asserts byte-identical
# folded values across Python/Node/Java (three independent implementations of the fold arithmetic) AND
# that every accept/reject flag holds.
def _case_folded(case):
log_blowup = case["log_blowup"]
log_max_height = case["log_max_height"]
commits = case["commit_phase_commits"]
betas = [list(b) for b in case["betas"]]
folded_per_query = []
for q in case["queries"]:
ro_full = build_ro_full(log_max_height, q["ro_top"])
folded = verify_query(log_blowup, log_max_height, commits, betas, q["index"], ro_full, q["layers"])
folded_per_query.append(folded)
return folded_per_query
def shared_vectors():
gt = load_ground_truth()
cases = []
for case in gt["cases"]:
cases.append({
"name": case["name"],
"logBlowup": case["log_blowup"],
"logMaxHeight": case["log_max_height"],
"numQueries": case["num_queries"],
"folded": _case_folded(case),
"finalPoly": list(case["final_poly"]),
"accept": verify_case(case),
"rejectSibling": not verify_case(case, "sibling"),
"rejectProof": not verify_case(case, "proof"),
"rejectBeta": not verify_case(case, "beta"),
"rejectFinal": not verify_case(case, "final"),
})
return {
"permZeros": M.permute([0] * 16),
"twoAdicGenerators": [F.two_adic_generator(b) for b in range(28)],
"cases": cases,
}
if __name__ == "__main__":
json.dump(shared_vectors(), sys.stdout)