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.
289 lines
13 KiB
Python
289 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
# Independent reference for the GENERIC AIR constraint / quotient-consistency check (component (e)) that
|
|
# a p3-uni-stark STARK verifier performs, for the PQ STARK-verify precompile 0x0AE8 (direct SP1/Plonky3
|
|
# inner FRI/STARK verify). See docs/AERE-STARK-VERIFIER-PORT-SPEC.md section 6 and spec-stark-air.md.
|
|
#
|
|
# ============================ HONEST SCOPE (read first) ============================
|
|
# This module implements the GENERIC quotient-consistency MECHANISM that p3-uni-stark's verify() runs
|
|
# AFTER the PCS opening argument (verifier.rs lines 90-141): given the out-of-domain trace openings at
|
|
# zeta (trace_local) and at g*zeta (trace_next), a random challenge alpha, the quotient-chunk openings,
|
|
# and the trace-domain degree, it (1) folds the AIR's constraints with alpha into a single value
|
|
# folded_constraints(zeta) (Horner: acc = acc*alpha + constraint), (2) reconstructs quotient(zeta) from
|
|
# the chunk openings and the split-domain normalization zps, (3) computes the vanishing polynomial
|
|
# Z_H(zeta) and the first/last/transition selectors, and (4) checks the identity
|
|
# folded_constraints(zeta) == Z_H(zeta) * quotient(zeta) (equivalently folded * inv_zeroifier == quotient),
|
|
# all over F_{p^4}. It is CONFIRMED-FROM-SOURCE against the pinned Plonky3 p3-uni-stark 0.4.3-succinct
|
|
# verifier.rs / p3-commit domain.rs / p3-air folder.rs, and a real known-answer test PASSES (see
|
|
# conformance() and test_air_quotient.py): the ground-truth extractor (pq-stark/airquotient-extractor)
|
|
# ran the pinned p3-uni-stark PROVER + VERIFIER on two KNOWN example AIRs (the Fibonacci AIR from
|
|
# Plonky3's own tests/fib_air.rs, and a degree-3 multiply AIR matching tests/mul_air.rs), confirmed the
|
|
# library ACCEPTS, and emitted alpha/zeta/openings; this reference reproduces the accept and rejects
|
|
# tampered inputs (a corrupted trace opening, a wrong quotient chunk, a wrong alpha).
|
|
#
|
|
# WHAT IS AND IS NOT PORTED (the honest boundary):
|
|
# - PORTED + CONFIRMED here (component (e) GENERIC mechanism): the constraint-fold + quotient
|
|
# reconstruction + Z_H + selectors + the identity check, for a SUPPLIED AIR constraint evaluator.
|
|
# The constraint evaluators for the two example AIRs are transcribed BY HAND from their eval().
|
|
# - NOT ported (the SPECIFIC piece that keeps the precompile fail-closed): the SP1 RECURSION AIR (its
|
|
# exact multi-thousand-constraint set, interactions/permutation argument, public-value layout) and
|
|
# the verifying-key digest that commits to it. That is a large, program-specific, multi-week port
|
|
# needing the SP1 toolchain, and is NOT here. So this passing GENERIC KAT does NOT let the precompile
|
|
# verify a real SP1 proof: the top-level 0x0AE8 STAYS FAIL-CLOSED for real proofs (see the precompile
|
|
# StarkConstraints.SP1_RECURSION_AIR_PORTED = false gate).
|
|
#
|
|
# Source (pinned, checksum-matched to the repo Cargo.lock):
|
|
# p3-uni-stark 0.4.3-succinct verifier.rs (the quotient-consistency check)
|
|
# p3-commit 0.4.3-succinct domain.rs (TwoAdicMultiplicativeCoset selectors / zp_at_point / split)
|
|
# p3-air 0.4.3-succinct folder.rs, air.rs (VerifierConstraintFolder Horner fold; when_* filters)
|
|
|
|
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)
|
|
|
|
GROUND_TRUTH = os.path.join(HERE, "air_quotient_ground_truth.json")
|
|
|
|
P = F.P
|
|
ONE = F.ext_from_base(1)
|
|
|
|
|
|
# ============================ domain / selector math (p3-commit domain.rs) ============================
|
|
# Elements of F_{p^4} are 4-int lists [c0,c1,c2,c3] (matching BinomialExtensionField<BabyBear,4>).
|
|
|
|
def ext_exp_power_of_2(x, log_n):
|
|
"""x^(2^log_n) in F_{p^4} by repeated squaring (Field::exp_power_of_2)."""
|
|
r = list(x)
|
|
for _ in range(log_n):
|
|
r = F.ext_mul(r, r)
|
|
return r
|
|
|
|
|
|
def ext_div(a, b):
|
|
return F.ext_mul(a, F.ext_inv(b))
|
|
|
|
|
|
def zp_at_point(log_n, shift_base, point_ext):
|
|
"""Z_D(point) for a TwoAdicMultiplicativeCoset D of size 2^log_n and (base-field) shift:
|
|
(point * shift^-1)^(2^log_n) - 1. Mirrors domain.rs zp_at_point."""
|
|
shift_inv = F.ext_from_base(F.inv(shift_base))
|
|
return F.ext_sub(ext_exp_power_of_2(F.ext_mul(point_ext, shift_inv), log_n), ONE)
|
|
|
|
|
|
def selectors_at_point(degree_bits, zeta):
|
|
"""domain.rs selectors_at_point for the trace domain (shift = 1, log_n = degree_bits).
|
|
Returns (is_first_row, is_last_row, is_transition, inv_zeroifier), all F_{p^4}."""
|
|
g = F.two_adic_generator(degree_bits)
|
|
g_inv = F.ext_from_base(F.inv(g))
|
|
unshifted = list(zeta) # shift = 1
|
|
z_h = F.ext_sub(ext_exp_power_of_2(unshifted, degree_bits), ONE)
|
|
is_first = ext_div(z_h, F.ext_sub(unshifted, ONE))
|
|
is_last = ext_div(z_h, F.ext_sub(unshifted, g_inv))
|
|
is_transition = F.ext_sub(unshifted, g_inv)
|
|
inv_zeroifier = F.ext_inv(z_h)
|
|
return is_first, is_last, is_transition, inv_zeroifier
|
|
|
|
|
|
def monomial(e_i):
|
|
"""The F_{p^4} basis element x^{e_i} (Challenge::monomial): [0,..,1 at e_i,..,0]."""
|
|
m = [0, 0, 0, 0]
|
|
m[e_i] = 1
|
|
return m
|
|
|
|
|
|
def reconstruct_quotient(degree_bits, quotient_chunks, zeta):
|
|
"""Reconstruct quotient(zeta) from the chunk openings (verifier.rs lines 90-116).
|
|
|
|
quotient_domain: log_n = degree_bits + log_quotient_degree, shift = GENERATOR (base).
|
|
chunk i domain: log_n = degree_bits, shift = GENERATOR * gen_q^i, gen_q = two_adic_generator(log_n_q).
|
|
zps[i] = prod_{j != i} zp_j(zeta) * zp_j(first_point_i)^-1.
|
|
quotient = sum_i zps[i] * sum_{e} monomial(e) * chunk[i][e]."""
|
|
quotient_degree = len(quotient_chunks)
|
|
log_quotient_degree = quotient_degree.bit_length() - 1
|
|
assert (1 << log_quotient_degree) == quotient_degree, "quotient_degree must be a power of two"
|
|
log_n_q = degree_bits + log_quotient_degree
|
|
gen_q = F.two_adic_generator(log_n_q)
|
|
# chunk-domain base-field shifts
|
|
shifts = [F.mul(F.GENERATOR, F.pow_(gen_q, i)) for i in range(quotient_degree)]
|
|
|
|
zps = []
|
|
for i in range(quotient_degree):
|
|
acc = list(ONE)
|
|
first_point_i = F.ext_from_base(shifts[i]) # domain_i.first_point() = shift_i
|
|
for j in range(quotient_degree):
|
|
if j == i:
|
|
continue
|
|
num = zp_at_point(degree_bits, shifts[j], zeta)
|
|
den = zp_at_point(degree_bits, shifts[j], first_point_i)
|
|
acc = F.ext_mul(acc, F.ext_mul(num, F.ext_inv(den)))
|
|
zps.append(acc)
|
|
|
|
quotient = [0, 0, 0, 0]
|
|
for ch_i, ch in enumerate(quotient_chunks):
|
|
for e_i, c in enumerate(ch):
|
|
term = F.ext_mul(F.ext_mul(zps[ch_i], monomial(e_i)), c)
|
|
quotient = F.ext_add(quotient, term)
|
|
return quotient
|
|
|
|
|
|
# ============================ example AIR constraint evaluators (transcribed by hand) ============
|
|
# Each returns folded_constraints(zeta): the AIR's constraints folded with alpha via Horner, exactly as
|
|
# VerifierConstraintFolder.assert_zero does (acc = acc*alpha + constraint), in the eval() emission order.
|
|
|
|
def _horner(constraints, alpha):
|
|
acc = [0, 0, 0, 0]
|
|
for c in constraints:
|
|
acc = F.ext_add(F.ext_mul(acc, alpha), c)
|
|
return acc
|
|
|
|
|
|
def fold_fibonacci(tl, tn, pis, sel_first, sel_last, sel_trans, alpha):
|
|
"""tests/fib_air.rs eval(): columns [left,right], public values [a,b,x]. Constraint order:
|
|
is_first*(left-a), is_first*(right-b), is_trans*(right-next.left),
|
|
is_trans*(left+right-next.right), is_last*(right-x)."""
|
|
a = F.ext_from_base(pis[0])
|
|
b = F.ext_from_base(pis[1])
|
|
x = F.ext_from_base(pis[2])
|
|
left, right = tl[0], tl[1]
|
|
nleft, nright = tn[0], tn[1]
|
|
c1 = F.ext_mul(sel_first, F.ext_sub(left, a))
|
|
c2 = F.ext_mul(sel_first, F.ext_sub(right, b))
|
|
c3 = F.ext_mul(sel_trans, F.ext_sub(right, nleft))
|
|
c4 = F.ext_mul(sel_trans, F.ext_sub(F.ext_add(left, right), nright))
|
|
c5 = F.ext_mul(sel_last, F.ext_sub(right, x))
|
|
return _horner([c1, c2, c3, c4, c5], alpha)
|
|
|
|
|
|
def fold_mul_deg3(tl, tn, pis, sel_first, sel_last, sel_trans, alpha):
|
|
"""tests/mul_air.rs eval() (REPETITIONS=1, degree=3): columns [a,b,c]. Constraint order:
|
|
a^2*b - c (all rows), is_first*((a*a+1)-b), is_trans*((a+1)-next_a)."""
|
|
a, b, c = tl[0], tl[1], tl[2]
|
|
next_a = tn[0]
|
|
c1 = F.ext_sub(F.ext_mul(F.ext_mul(a, a), b), c) # a^2*b - c
|
|
c2 = F.ext_mul(sel_first, F.ext_sub(F.ext_add(F.ext_mul(a, a), ONE), b)) # is_first*(a*a+1 - b)
|
|
c3 = F.ext_mul(sel_trans, F.ext_sub(F.ext_add(a, ONE), next_a)) # is_trans*(a+1 - next_a)
|
|
return _horner([c1, c2, c3], alpha)
|
|
|
|
|
|
AIR_FOLDERS = {
|
|
"fibonacci": fold_fibonacci,
|
|
"mul_deg3": fold_mul_deg3,
|
|
}
|
|
|
|
|
|
# ============================ the generic quotient-consistency check ============================
|
|
|
|
def check_identity(case):
|
|
"""Reproduce p3-uni-stark verify()'s quotient-consistency check for one case. Returns True iff
|
|
folded_constraints(zeta) * inv_zeroifier == quotient(zeta) (i.e. == Z_H(zeta)*quotient(zeta))."""
|
|
degree_bits = case["degree_bits"]
|
|
alpha = list(case["alpha"])
|
|
zeta = list(case["zeta"])
|
|
pis = list(case["public_values"])
|
|
tl = [list(v) for v in case["trace_local"]]
|
|
tn = [list(v) for v in case["trace_next"]]
|
|
chunks = [[list(v) for v in ch] for ch in case["quotient_chunks"]]
|
|
|
|
is_first, is_last, is_transition, inv_zeroifier = selectors_at_point(degree_bits, zeta)
|
|
quotient = reconstruct_quotient(degree_bits, chunks, zeta)
|
|
folder = AIR_FOLDERS[case["air"]]
|
|
folded = folder(tl, tn, pis, is_first, is_last, is_transition, alpha)
|
|
lhs = F.ext_mul(folded, inv_zeroifier)
|
|
return F.ext_eq(lhs, quotient)
|
|
|
|
|
|
def check_case(case, tamper=None):
|
|
"""Run the identity check, optionally injecting a fault to demonstrate rejection: 'trace'
|
|
(corrupt a trace opening), 'quotient' (corrupt a quotient chunk opening), 'alpha' (wrong alpha)."""
|
|
c = json.loads(json.dumps(case)) # deep copy
|
|
if tamper == "trace":
|
|
c["trace_local"][0][0] = (c["trace_local"][0][0] + 1) % P
|
|
elif tamper == "quotient":
|
|
c["quotient_chunks"][0][0][0] = (c["quotient_chunks"][0][0][0] + 1) % P
|
|
elif tamper == "alpha":
|
|
c["alpha"][0] = (c["alpha"][0] + 1) % P
|
|
return check_identity(c)
|
|
|
|
|
|
# ============================ conformance KAT (real ground truth) ============================
|
|
|
|
def load_ground_truth():
|
|
with open(GROUND_TRUTH) as f:
|
|
return json.load(f)
|
|
|
|
|
|
def conformance_kats():
|
|
"""(passed, total) after: (0) perm/generator sanity tying the ground truth to the CONFIRMED
|
|
Poseidon2 permutation + component (a); (1) accepting every genuine proof's identity; (2) rejecting
|
|
each of three tamper variants per case."""
|
|
gt = load_ground_truth()
|
|
passed = 0
|
|
total = 0
|
|
|
|
# (0) sanity
|
|
import mmcs_babybear_reference as M
|
|
total += 1
|
|
if M.permute([0] * 16) == gt["perm_zeros"]:
|
|
passed += 1
|
|
total += 1
|
|
if gt["val_generator"] == F.GENERATOR:
|
|
passed += 1
|
|
|
|
# (1) accept genuine identity; (2) reject tampered.
|
|
for case in gt["cases"]:
|
|
total += 1
|
|
if case["library_accept"] and check_case(case):
|
|
passed += 1
|
|
for tamper in ("trace", "quotient", "alpha"):
|
|
total += 1
|
|
if not check_case(case, tamper):
|
|
passed += 1
|
|
return passed, total
|
|
|
|
|
|
# ============================ shared cross-language vector set ============================
|
|
# All three languages emit, per case: the accept flag, the three tamper-reject flags, and the recomputed
|
|
# quotient(zeta) and folded_constraints(zeta) (F_{p^4}). The harness asserts byte-identical values across
|
|
# Python/Node/Java (three independent implementations) AND that every accept/reject flag holds.
|
|
|
|
def _case_derived(case):
|
|
degree_bits = case["degree_bits"]
|
|
zeta = list(case["zeta"])
|
|
chunks = [[list(v) for v in ch] for ch in case["quotient_chunks"]]
|
|
is_first, is_last, is_transition, inv_zeroifier = selectors_at_point(degree_bits, zeta)
|
|
quotient = reconstruct_quotient(degree_bits, chunks, zeta)
|
|
folder = AIR_FOLDERS[case["air"]]
|
|
folded = folder([list(v) for v in case["trace_local"]],
|
|
[list(v) for v in case["trace_next"]],
|
|
list(case["public_values"]),
|
|
is_first, is_last, is_transition, list(case["alpha"]))
|
|
return {"quotient": quotient, "folded": folded, "invZeroifier": inv_zeroifier}
|
|
|
|
|
|
def shared_vectors():
|
|
gt = load_ground_truth()
|
|
cases = []
|
|
for case in gt["cases"]:
|
|
d = _case_derived(case)
|
|
cases.append({
|
|
"name": case["name"],
|
|
"air": case["air"],
|
|
"degreeBits": case["degree_bits"],
|
|
"quotientDegree": case["quotient_degree"],
|
|
"quotient": d["quotient"],
|
|
"folded": d["folded"],
|
|
"invZeroifier": d["invZeroifier"],
|
|
"accept": check_case(case),
|
|
"rejectTrace": not check_case(case, "trace"),
|
|
"rejectQuotient": not check_case(case, "quotient"),
|
|
"rejectAlpha": not check_case(case, "alpha"),
|
|
})
|
|
return {"valGenerator": F.GENERATOR, "cases": cases}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
json.dump(shared_vectors(), sys.stdout)
|