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

110 lines
5.7 KiB
Python

#!/usr/bin/env python3
# Independent reference for the FRI query-index derivation used by the PQ STARK-verify
# precompile 0x0AE8 (direct SP1/Plonky3 inner FRI/STARK verify). This module implements ONE
# self-contained, constant-free slice of component (d) in
# docs/AERE-STARK-VERIFIER-PORT-SPEC.md: turning Fiat-Shamir challenger output into FRI query
# indices, and the per-layer folding-index walk each query follows.
#
# HONEST SCOPE. This is NOT a STARK verifier and does not verify anything. It reproduces the
# public Plonky3 p3-fri index arithmetic (sample_bits + the arity-2 folding walk). It depends on
# NO secret round constants, so it can be validated offline against hand-computed golden vectors
# and cross-checked against independent implementations (Node and Java). Conformance to a real
# SP1 v6.1.0 trace is [MEASURE] (needs the Poseidon2 challenger + an exported proof); see the
# port spec section 8 for the exact validation command. The top-level 0x0AE8 verifier stays
# fail-closed regardless of this module.
#
# Plonky3 semantics reproduced (mark [VERIFY] against p3-fri / p3-challenger at the pinned SP1
# v6.1.0 Plonky3 revision):
# - sample_bits(bits): low `bits` bits of a sampled base-field element's canonical u32
# representative. [VERIFY] LSB (not MSB) and that one BabyBear sample supplies enough bits
# (log_max_height < 31 for the pinned config).
# - folding walk: arity 2, index_sibling = index ^ 1, index_pair = index >> 1 per layer.
# [VERIFY] against p3-fri verifier.rs verify_query.
# - num_fold_rounds = log_max_height - log_final_poly_len; after that many folds the index
# collapses into the final polynomial's domain [0, 2^log_final_poly_len). [VERIFY] the final
# poly length for the pinned config.
from dataclasses import dataclass, asdict
from typing import List
# BabyBear prime, for range-checking a canonical field representative fed to sample_bits.
BABYBEAR_P = 2013265921 # 2^31 - 2^27 + 1 = 0x78000001
def sample_bits(canonical_u32: int, bits: int) -> int:
"""Reduce a sampled BabyBear field element (its canonical u32 representative, in [0, p)) to a
query index in [0, 2^bits) by taking the low `bits` bits. Mirrors Plonky3
CanSampleBits::sample_bits for a DuplexChallenger over BabyBear.
[VERIFY] Plonky3 masks the LOW bits of as_canonical_u32(); confirm LSB vs MSB against
p3-challenger. bits must be < 31 so a single 31-bit BabyBear sample supplies them."""
if bits < 0:
raise ValueError("bits must be non-negative")
if bits >= 31:
# A single BabyBear element only carries ~31 bits; larger indices would need multiple
# samples. The pinned SP1 configs keep log_max_height well under 31, so we guard here
# rather than silently truncate.
raise ValueError("bits >= 31 needs multiple field samples; not supported for pinned config")
if canonical_u32 < 0 or canonical_u32 >= BABYBEAR_P:
raise ValueError("canonical_u32 must be a canonical BabyBear representative in [0, p)")
return canonical_u32 & ((1 << bits) - 1)
@dataclass(frozen=True)
class FoldStep:
layer: int # 0-based FRI folding round
index: int # this query's index within the current (folded) domain
sibling: int # index ^ 1, the paired evaluation opened alongside `index`
index_pair: int # index >> 1, the Merkle-open position in the half-size next layer
parity: int # index & 1, which of the sibling pair is this query's own evaluation
def walk(index: int, log_max_height: int, log_final_poly_len: int = 0) -> List[FoldStep]:
"""Produce the per-layer folding-index walk for one FRI query.
Given the sampled `index` in [0, 2^log_max_height) and the final polynomial log-length, return
the sequence of FoldStep records, one per folding round. num_fold_rounds =
log_max_height - log_final_poly_len. This is the index bookkeeping a FRI verifier follows to
know, at every layer, which sibling pair to open and which folded value is its own. It does NOT
perform the fold arithmetic or any Merkle/Poseidon2 opening (those are delegated, port spec
section 5)."""
if log_max_height < 0 or log_final_poly_len < 0:
raise ValueError("log heights must be non-negative")
if log_final_poly_len > log_max_height:
raise ValueError("log_final_poly_len must not exceed log_max_height")
if index < 0 or index >= (1 << log_max_height):
raise ValueError("index out of range for log_max_height")
num_fold_rounds = log_max_height - log_final_poly_len
steps: List[FoldStep] = []
cur = index
for layer in range(num_fold_rounds):
steps.append(
FoldStep(
layer=layer,
index=cur,
sibling=cur ^ 1,
index_pair=cur >> 1,
parity=cur & 1,
)
)
cur = cur >> 1
return steps
def final_index(index: int, log_max_height: int, log_final_poly_len: int = 0) -> int:
"""The terminal index after all folds; must land in the final polynomial domain
[0, 2^log_final_poly_len)."""
num_fold_rounds = log_max_height - log_final_poly_len
return index >> num_fold_rounds
def derive_query_indices(sampled_canonical_u32: List[int], log_max_height: int) -> List[int]:
"""Turn a list of challenger-sampled field representatives (one per query) into query indices.
In a real verifier the samples come out of the Poseidon2 duplex challenger (delegated); here
they are supplied so the index reduction can be tested independently of the sponge."""
return [sample_bits(s, log_max_height) for s in sampled_canonical_u32]
def walk_as_dicts(index: int, log_max_height: int, log_final_poly_len: int = 0):
return [asdict(s) for s in walk(index, log_max_height, log_final_poly_len)]