aere-research/pq-stark/fri-extractor/src/main.rs
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

237 lines
10 KiB
Rust

// Ground-truth extractor for the FRI low-degree test (component (d)) that SP1/Plonky3 use over
// BabyBear, for the PQ STARK-verify precompile 0x0AE8. It builds the EXACT SP1 inner FRI config
// Val = BabyBear
// Challenge = BinomialExtensionField<BabyBear, 4> (the FRI folding field EF)
// Perm = Poseidon2<BabyBear, Poseidon2ExternalMatrixGeneral, DiffusionMatrixBabyBear, 16, 7>
// MyHash = PaddingFreeSponge<Perm, 16, 8, 8>
// MyCompress = TruncatedPermutation<Perm, 2, 8, 16>
// ValMmcs = FieldMerkleTreeMmcs<Packing, Packing, MyHash, MyCompress, 8> (component (c), CONFIRMED)
// FriMmcs = ExtensionMmcs<Val, Challenge, ValMmcs> (the commit-phase MMCS over EF)
// Challenger = DuplexChallenger<BabyBear, Perm, 16, 8> (SP1 inner challenger, component (f))
// The permutation is built with Xoroshiro128Plus::seed_from_u64(1) via new_from_rng_128, the SAME
// deterministic construction the CONFIRMED Poseidon2/MMCS references use (a perm_zeros sanity KAT is
// emitted so the harness can assert it before trusting anything).
//
// For each FRI case it: constructs a genuinely low-degree codeword (an RS/LDE of a random low-degree
// polynomial over the two-adic subgroup, in bit-reversed order), runs the pinned p3-fri PROVER to emit
// a real FriProof (commit-phase roots, per-query openings + MMCS proofs, betas, final poly, pow
// witness), then runs the pinned p3-fri VERIFIER (verify_shape_and_sample_challenges +
// verify_challenges) to confirm the library itself accepts. It emits every value the reference FRI
// verifier needs to reproduce verify_query: the betas and query indices (which the real transcript /
// component (f) derives, supplied here as inputs), the reduced openings, and per-query per-layer
// sibling values + MMCS opening proofs. Field elements are printed as canonical u32.
use p3_baby_bear::{BabyBear, DiffusionMatrixBabyBear};
use p3_challenger::DuplexChallenger;
use p3_commit::ExtensionMmcs;
use p3_dft::{Radix2Dit, TwoAdicSubgroupDft};
use p3_field::extension::BinomialExtensionField;
use p3_field::{AbstractExtensionField, AbstractField, Field, PrimeField32, TwoAdicField};
use p3_fri::verifier::{verify_challenges, verify_shape_and_sample_challenges};
use p3_fri::FriConfig;
use p3_merkle_tree::FieldMerkleTreeMmcs;
use p3_poseidon2::{Poseidon2, Poseidon2ExternalMatrixGeneral};
use p3_symmetric::{PaddingFreeSponge, Permutation, TruncatedPermutation};
use p3_util::reverse_slice_index_bits;
use rand::SeedableRng;
use rand_xoshiro::Xoroshiro128Plus;
type Val = BabyBear;
type Challenge = BinomialExtensionField<BabyBear, 4>;
type Perm = Poseidon2<Val, Poseidon2ExternalMatrixGeneral, DiffusionMatrixBabyBear, 16, 7>;
type MyHash = PaddingFreeSponge<Perm, 16, 8, 8>;
type MyCompress = TruncatedPermutation<Perm, 2, 8, 16>;
type ValMmcs =
FieldMerkleTreeMmcs<<Val as Field>::Packing, <Val as Field>::Packing, MyHash, MyCompress, 8>;
type ChallengeMmcs = ExtensionMmcs<Val, Challenge, ValMmcs>;
type Challenger = DuplexChallenger<Val, Perm, 16, 8>;
fn u32s(row: &[Val]) -> Vec<u32> {
row.iter().map(|f| f.as_canonical_u32()).collect()
}
// An extension field element as its 4 canonical base coords [c0, c1, c2, c3].
fn ef_u32(e: &Challenge) -> Vec<u32> {
let base: &[Val] = <Challenge as AbstractExtensionField<Val>>::as_base_slice(e);
base.iter().map(|f| f.as_canonical_u32()).collect()
}
fn ja(v: &[u32]) -> String {
let s: Vec<String> = v.iter().map(|x| x.to_string()).collect();
format!("[{}]", s.join(","))
}
fn jaa(v: &[Vec<u32>]) -> String {
let s: Vec<String> = v.iter().map(|x| ja(x)).collect();
format!("[{}]", s.join(","))
}
struct FriCase {
name: &'static str,
log_blowup: usize,
log_max_height: usize,
num_queries: usize,
pow_bits: usize,
seed: u64,
}
fn main() {
// Deterministic perm identical to the CONFIRMED Poseidon2 reference (seed_from_u64(1)).
let mut rng = Xoroshiro128Plus::seed_from_u64(1);
let perm = Perm::new_from_rng_128(
Poseidon2ExternalMatrixGeneral,
DiffusionMatrixBabyBear,
&mut rng,
);
let mut out = String::new();
out.push('{');
// ---- perm sanity KAT: permute([0;16]) must equal the confirmed Poseidon2 "zeros" vector ----
let zeros = perm.permute([Val::zero(); 16]);
out.push_str(&format!("\"perm_zeros\":{},", ja(&u32s(&zeros))));
// ---- two-adic generators g(bits) for bits 0..=27 (confirms component (a) twoAdicGenerator) ----
let gens: Vec<u32> = (0..=27)
.map(|b| Val::two_adic_generator(b).as_canonical_u32())
.collect();
out.push_str(&format!("\"two_adic_generators\":{},", ja(&gens)));
// ---- FRI cases ----
let cases = vec![
FriCase { name: "blowup1_h6_q4", log_blowup: 1, log_max_height: 6, num_queries: 4, pow_bits: 1, seed: 42 },
FriCase { name: "blowup2_h7_q5", log_blowup: 2, log_max_height: 7, num_queries: 5, pow_bits: 0, seed: 7 },
FriCase { name: "blowup1_h8_q6", log_blowup: 1, log_max_height: 8, num_queries: 6, pow_bits: 3, seed: 12345 },
];
out.push_str("\"cases\":[");
for (ci, case) in cases.iter().enumerate() {
if ci > 0 {
out.push(',');
}
out.push_str(&run_case(&perm, case));
}
out.push_str("]}");
println!("{}", out);
}
fn run_case(perm: &Perm, case: &FriCase) -> String {
let hash = MyHash::new(perm.clone());
let compress = MyCompress::new(perm.clone());
let val_mmcs = ValMmcs::new(hash, compress);
let challenge_mmcs = ChallengeMmcs::new(val_mmcs);
let config = FriConfig {
log_blowup: case.log_blowup,
num_queries: case.num_queries,
proof_of_work_bits: case.pow_bits,
mmcs: challenge_mmcs,
};
let n = 1usize << case.log_max_height;
let k = 1usize << (case.log_max_height - case.log_blowup); // message length (degree bound)
// Build a genuinely low-degree codeword: coeffs of a degree-<k poly (rest zero), DFT to evals over
// the size-n subgroup, then bit-reverse (the order the FRI prover's `input` expects).
let mut state: u64 = case.seed.wrapping_mul(0x9E3779B97F4A7C15).wrapping_add(1);
let mut next = || {
// simple deterministic LCG-ish generator for reproducible coeffs
state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
((state >> 33) as u32) % (BabyBear::ORDER_U32)
};
let mut coeffs: Vec<Challenge> = Vec::with_capacity(n);
for i in 0..n {
if i < k {
let c0 = Val::from_canonical_u32(next());
let c1 = Val::from_canonical_u32(next());
let c2 = Val::from_canonical_u32(next());
let c3 = Val::from_canonical_u32(next());
coeffs.push(Challenge::from_base_slice(&[c0, c1, c2, c3]));
} else {
coeffs.push(Challenge::zero());
}
}
let dft = Radix2Dit::<Challenge>::default();
let mut evals = dft.dft(coeffs); // natural order over the subgroup
reverse_slice_index_bits(&mut evals); // bit-reversed order (prover input convention)
// input[log_max_height] = Some(codeword); all other heights None (single-codeword LDT).
let mut input: [Option<Vec<Challenge>>; 32] = core::array::from_fn(|_| None);
input[case.log_max_height] = Some(evals.clone());
// ---- PROVE (pinned p3-fri prover) ----
let mut p_challenger = Challenger::new(perm.clone());
let (proof, prover_indices) = p3_fri::prover::prove(&config, &input, &mut p_challenger);
// ---- re-derive challenges via the pinned verifier's transcript (component (f)) ----
let mut v_challenger = Challenger::new(perm.clone());
let challenges = verify_shape_and_sample_challenges(&config, &proof, &mut v_challenger)
.expect("verify_shape_and_sample_challenges must succeed");
assert_eq!(
challenges.query_indices, prover_indices,
"verifier-derived query indices must equal the prover's"
);
// ---- reduced openings: single codeword => ro[log_max_height] = codeword value at index ----
let mut ros: Vec<[Challenge; 32]> = Vec::new();
for &index in &challenges.query_indices {
let mut ro = [Challenge::zero(); 32];
ro[case.log_max_height] = evals[index];
ros.push(ro);
}
// ---- VERIFY (pinned p3-fri verifier: the fold + opening relations) : must accept ----
verify_challenges(&config, &proof, &challenges, &ros)
.expect("library verify_challenges must accept the honest proof");
// ---- emit everything the reference FRI verifier needs ----
let commits: Vec<Vec<u32>> = proof
.commit_phase_commits
.iter()
.map(|c| {
let arr: [Val; 8] = (*c).into();
u32s(&arr)
})
.collect();
let betas: Vec<Vec<u32>> = challenges.betas.iter().map(ef_u32).collect();
let final_poly = ef_u32(&proof.final_poly);
// per-query records
let mut queries_json: Vec<String> = Vec::new();
for (qi, &index) in challenges.query_indices.iter().enumerate() {
let ro_top = ef_u32(&ros[qi][case.log_max_height]);
let qp = &proof.query_proofs[qi];
let mut layers_json: Vec<String> = Vec::new();
for step in &qp.commit_phase_openings {
let sib = ef_u32(&step.sibling_value);
let op: Vec<Vec<u32>> = step.opening_proof.iter().map(|d| u32s(d)).collect();
layers_json.push(format!(
"{{\"sibling_value\":{},\"opening_proof\":{}}}",
ja(&sib),
jaa(&op)
));
}
queries_json.push(format!(
"{{\"index\":{},\"ro_top\":{},\"layers\":[{}]}}",
index,
ja(&ro_top),
layers_json.join(",")
));
}
format!(
"{{\"name\":\"{}\",\"log_blowup\":{},\"log_max_height\":{},\"num_queries\":{},\"pow_bits\":{},\"commit_phase_commits\":{},\"betas\":{},\"final_poly\":{},\"queries\":[{}]}}",
case.name,
case.log_blowup,
case.log_max_height,
case.num_queries,
case.pow_bits,
jaa(&commits),
jaa(&betas),
ja(&final_poly),
queries_json.join(",")
)
}