use alloc::vec::Vec;
use p3_challenger::{CanObserve, FieldChallenger, GrindingChallenger};
use p3_commit::Mmcs;
use p3_field::{ExtensionField, Field, HornerIter};
use p3_matrix::Matrix;
use p3_multilinear_util::point::Point;
use p3_zk_codes::ZkEncodingWithRandomness;
use rand::distr::{Distribution, StandardUniform};
use rand::{Rng, RngExt};
use super::data::ZkSumcheckData;
use super::prover::stack_codewords;
use super::verifier::ZkVerifier;
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
pub fn simulate_classic_unpacked<F, EF, Enc, M, Challenger, R>(
challenger: &mut Challenger,
verifier: &ZkVerifier<F, EF>,
folding_factor: usize,
pow_bits: usize,
encoding: &Enc,
mmcs: &M,
rng: &mut R,
) -> (ZkSumcheckData<F, EF>, M::Commitment, Point<EF>)
where
F: Field,
EF: ExtensionField<F>,
Enc: ZkEncodingWithRandomness<EF>,
Enc::Codeword: Matrix<EF>,
M: Mmcs<EF>,
Challenger: FieldChallenger<F> + GrindingChallenger<Witness = F> + CanObserve<M::Commitment>,
R: Rng,
StandardUniform: Distribution<EF>,
{
let k = folding_factor;
let ell_zk = encoding.message_len();
assert!(F::TWO != F::ZERO, "Lemma 6.4 requires char(F) != 2");
assert!(
ell_zk >= 3,
"mask degree ell_zk - 1 must cover the degree-2 plain piece (ell_zk >= 3)",
);
assert!(k >= 1, "sumcheck requires at least one round");
let alpha: EF = challenger.sample_algebra_element();
let mu = verifier.sum(alpha);
let masks: Vec<Vec<EF>> = (0..k).map(|_| encoding.sample_message(rng)).collect();
let codewords: Vec<Enc::Codeword> = masks
.iter()
.map(|mask| {
let randomness = encoding.sample_randomness(rng);
encoding.encode_with_randomness(mask, &randomness)
})
.collect();
let (mask_commitment, _prover_data) = mmcs.commit_matrix(stack_codewords(&codewords));
challenger.observe(mask_commitment.clone());
let two_to_k_minus_1 = EF::TWO.exp_u64((k - 1) as u64);
let mu_tilde: EF = two_to_k_minus_1
* masks
.iter()
.map(|m| m[0].double() + m[1..].iter().copied().sum::<EF>())
.sum::<EF>();
challenger.observe_algebra_element(mu_tilde);
let eps: EF = challenger.sample_algebra_element();
let h_size = ell_zk.max(3);
let wire_size = h_size - 1;
let mut zk_data = ZkSumcheckData::<F, EF> {
mu_tilde,
ell_zk,
round_coefficients: Vec::with_capacity(k),
pow_witnesses: Vec::with_capacity(if pow_bits > 0 { k } else { 0 }),
};
let mut randomness: Vec<EF> = Vec::with_capacity(k);
let mut target: EF = eps * mu + mu_tilde;
for _ in 0..k {
let wire: Vec<EF> = (0..wire_size).map(|_| rng.random::<EF>()).collect();
challenger.observe_algebra_slice(&wire);
if pow_bits > 0 {
zk_data.pow_witnesses.push(challenger.grind(pow_bits));
}
let gamma_j: EF = challenger.sample_algebra_element();
let c0 = wire[0];
let high_sum: EF = wire[1..].iter().copied().sum();
let c1 = target - c0.double() - high_sum;
target = core::iter::once(c0)
.chain(core::iter::once(c1))
.chain(wire[1..].iter().copied())
.horner(gamma_j);
zk_data.round_coefficients.push(wire);
randomness.push(gamma_j);
}
(zk_data, mask_commitment, Point::new(randomness))
}
#[cfg(test)]
mod tests {
use alloc::vec::Vec;
use p3_field::{BasedVectorSpace, Field, PackedValue, PrimeCharacteristicRing};
use p3_zk_codes::ZkEncoding;
use proptest::prelude::*;
use rand::rngs::SmallRng;
use rand::{RngExt, SeedableRng};
use super::*;
use crate::layout::TableShape;
use crate::strategy::VariableOrder;
use crate::zk::ZkVerifier;
use crate::zk::test_helpers::{EF, F, MyChallenger, MyMmcs, make_setup, run_prover};
fn escapes_f_subspace(x: EF) -> bool {
let coeffs: &[F] = EF::as_basis_coefficients_slice(&x);
coeffs[1..].iter().any(|c| *c != F::ZERO)
}
fn run_view_match_rs(
binding: VariableOrder,
n_vars: usize,
folding_factor: usize,
ell_zk: usize,
num_eqs: usize,
seed: u64,
) -> Result<(), &'static str> {
let pow_bits = 0;
let mut real_run = run_prover(
binding,
n_vars,
folding_factor,
ell_zk,
0,
num_eqs,
pow_bits,
seed,
);
let virtual_evals = real_run.virtual_evals.clone();
let zk_data_real = real_run.zk_data.clone();
let mask_commitment_real = real_run.mask_commitment.clone();
let _ = real_run
.verifier
.into_sumcheck::<MyMmcs, _>(
&zk_data_real,
&mask_commitment_real,
ell_zk,
folding_factor,
pow_bits,
&mut real_run.verifier_challenger,
)
.map_err(|_| "real prover transcript rejected by verifier")?;
let (perm, mmcs, encoding) = make_setup(seed, ell_zk);
let mut verifier_sim = match binding {
VariableOrder::Prefix => ZkVerifier::<F, EF>::new_prefix(&[TableShape::new(n_vars, 1)]),
VariableOrder::Suffix => ZkVerifier::<F, EF>::new_suffix(&[TableShape::new(n_vars, 1)]),
};
let mut sim_ch = MyChallenger::new(perm);
for &eval in &virtual_evals {
verifier_sim.add_virtual_eval(eval, &mut sim_ch);
}
let mut verifier_sim_ch = sim_ch.clone();
let mut sim_rng = SmallRng::seed_from_u64(seed.wrapping_add(2));
let (zk_data_sim, mask_commitment_sim, _gammas_sim) =
simulate_classic_unpacked::<F, EF, _, _, _, _>(
&mut sim_ch,
&verifier_sim,
folding_factor,
pow_bits,
&encoding,
&mmcs,
&mut sim_rng,
);
let _ = verifier_sim
.into_sumcheck::<MyMmcs, _>(
&zk_data_sim,
&mask_commitment_sim,
ell_zk,
folding_factor,
pow_bits,
&mut verifier_sim_ch,
)
.map_err(|_| "simulator transcript rejected by verifier")?;
if zk_data_real.mu_tilde != zk_data_sim.mu_tilde {
return Err("matched-RNG coupling: mu_tilde differs");
}
if mask_commitment_real != mask_commitment_sim {
return Err("matched-RNG coupling: mask commitment differs");
}
for wire in &zk_data_real.round_coefficients {
for &c in wire.iter().skip(2) {
if !escapes_f_subspace(c) {
return Err("real-prover wire[i >= 2] collapsed into the F-subspace");
}
}
}
for wire in &zk_data_sim.round_coefficients {
for &c in wire.iter().skip(2) {
if !escapes_f_subspace(c) {
return Err("simulator wire[i >= 2] collapsed into the F-subspace");
}
}
}
let t_zk = encoding.randomness_len();
let m = encoding.m;
let mut query_rng = SmallRng::seed_from_u64(seed.wrapping_add(5));
let mut sim_ans_rng = SmallRng::seed_from_u64(seed.wrapping_add(6));
for _ in 0..folding_factor {
let q_size = query_rng.random_range(1..=t_zk);
let mut positions: Vec<usize> = Vec::with_capacity(q_size);
while positions.len() < q_size {
let p = query_rng.random_range(0..m);
if !positions.contains(&p) {
positions.push(p);
}
}
let sim_answers: Vec<EF> = encoding.simulate(&positions, &mut sim_ans_rng);
if sim_answers.len() != positions.len() {
return Err("ZkEncoding::simulate returned wrong number of answers");
}
}
Ok(())
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(16))]
#[test]
fn prop_simulator_view_matches_real_rs_prefix(
n_vars in 3usize..=8,
ell_zk in 3usize..=5,
num_eqs in 1usize..=3,
seed in 0u64..1024,
) {
let k_pack = p3_util::log2_strict_usize(<F as Field>::Packing::WIDTH);
prop_assume!(n_vars > k_pack);
let folding_factor = 1 + (seed as usize % (n_vars - k_pack));
prop_assert!(
run_view_match_rs(VariableOrder::Prefix, n_vars, folding_factor, ell_zk, num_eqs, seed).is_ok()
);
}
#[test]
fn prop_simulator_view_matches_real_rs_suffix(
n_vars in 3usize..=8,
ell_zk in 3usize..=5,
num_eqs in 1usize..=3,
seed in 0u64..1024,
) {
let folding_factor = 1 + (seed as usize % (n_vars - 1).max(1));
prop_assert!(
run_view_match_rs(VariableOrder::Suffix, n_vars, folding_factor, ell_zk, num_eqs, seed).is_ok()
);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(8))]
#[test]
fn prop_simulator_invariants(
n_vars in 3usize..=6,
ell_zk in 4usize..=6,
num_eqs in 1usize..=2,
seed in 0u64..256,
) {
let folding_factor = 1 + (seed as usize % n_vars);
let (perm, mmcs, encoding) = make_setup(seed, ell_zk);
let mut data_rng = SmallRng::seed_from_u64(seed.wrapping_add(1));
let mut sim_challenger = MyChallenger::new(perm);
let mut verifier = ZkVerifier::<F, EF>::new_prefix(&[TableShape::new(n_vars, 1)]);
for _ in 0..num_eqs {
let eval: EF = data_rng.random();
verifier.add_virtual_eval(eval, &mut sim_challenger);
}
let mut verifier_replay_ch = sim_challenger.clone();
let pow_bits = 0;
let mut sim_rng = SmallRng::seed_from_u64(seed.wrapping_add(2));
let (sim_zk_data, mask_commitment, gammas) =
simulate_classic_unpacked::<F, EF, _, _, _, _>(
&mut sim_challenger,
&verifier,
folding_factor,
pow_bits,
&encoding,
&mmcs,
&mut sim_rng,
);
let expected_wire_size = ell_zk.max(3) - 1;
prop_assert_eq!(
sim_zk_data.round_coefficients.len(),
folding_factor,
"one wire per sumcheck round",
);
for (round_idx, wire) in sim_zk_data.round_coefficients.iter().enumerate() {
prop_assert_eq!(
wire.len(),
expected_wire_size,
"wire length mismatch in round {}",
round_idx,
);
}
prop_assert_eq!(sim_zk_data.ell_zk, ell_zk, "ell_zk header must match the encoding");
prop_assert!(
sim_zk_data.pow_witnesses.is_empty(),
"pow_witnesses must be empty when pow_bits == 0",
);
prop_assert_eq!(
gammas.as_slice().len(),
folding_factor,
"one challenge per round",
);
for (round_idx, wire) in sim_zk_data.round_coefficients.iter().enumerate() {
for (pos, &coeff) in wire.iter().enumerate().skip(2) {
prop_assert!(
escapes_f_subspace(coeff),
"simulator wire[{pos}] in round {round_idx} collapsed into the F-subspace",
);
}
}
let replay = verifier
.into_sumcheck::<MyMmcs, _>(
&sim_zk_data,
&mask_commitment,
ell_zk,
folding_factor,
pow_bits,
&mut verifier_replay_ch,
);
prop_assert!(
replay.is_ok(),
"verifier rejected the simulated transcript: {:?}",
replay.err(),
);
}
}
}