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.
110 lines
5.7 KiB
Python
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)]
|