/// This file implements the onchain (Ethereum's EVM) decider. use ark_crypto_primitives::sponge::Absorb; use ark_ec::{CurveGroup, Group}; use ark_ff::PrimeField; use ark_r1cs_std::fields::nonnative::params::OptimizationType; use ark_r1cs_std::{groups::GroupOpsBounds, prelude::CurveVar, ToConstraintFieldGadget}; use ark_snark::SNARK; use ark_std::rand::{CryptoRng, RngCore}; use ark_std::Zero; use core::marker::PhantomData; pub use super::decider_eth_circuit::{DeciderEthCircuit, KZGChallengesGadget}; use super::{circuits::CF2, nifs::NIFS, CommittedInstance, Nova}; use crate::commitment::{ kzg::Proof as KZGProof, pedersen::Params as PedersenParams, CommitmentScheme, }; use crate::folding::circuits::nonnative::point_to_nonnative_limbs_custom_opt; use crate::frontend::FCircuit; use crate::Error; use crate::{Decider as DeciderTrait, FoldingScheme}; #[derive(Debug, Clone, Eq, PartialEq)] pub struct Proof where C1: CurveGroup, CS1: CommitmentScheme, S: SNARK, { snark_proof: S::Proof, kzg_proofs: [CS1::Proof; 2], // cmT and r are values for the last fold, U_{i+1}=NIFS.V(r, U_i, u_i, cmT), and they are // checked in-circuit cmT: C1, r: C1::ScalarField, // the KZG challenges are provided by the prover, but in-circuit they are checked to match // the in-circuit computed computed ones. kzg_challenges: [C1::ScalarField; 2], } /// Onchain Decider, for ethereum use cases #[derive(Clone, Debug)] pub struct Decider { _c1: PhantomData, _gc1: PhantomData, _c2: PhantomData, _gc2: PhantomData, _fc: PhantomData, _cs1: PhantomData, _cs2: PhantomData, _s: PhantomData, _fs: PhantomData, } impl DeciderTrait for Decider where C1: CurveGroup, C2: CurveGroup, GC1: CurveVar> + ToConstraintFieldGadget>, GC2: CurveVar>, FC: FCircuit, CS1: CommitmentScheme< C1, ProverChallenge = C1::ScalarField, Challenge = C1::ScalarField, Proof = KZGProof, >, // KZG commitment, where challenge is C1::Fr elem // enforce that the CS2 is Pedersen commitment scheme, since we're at Ethereum's EVM decider CS2: CommitmentScheme>, S: SNARK, FS: FoldingScheme, ::BaseField: PrimeField, ::BaseField: PrimeField, ::ScalarField: Absorb, ::ScalarField: Absorb, C1: CurveGroup, for<'b> &'b GC2: GroupOpsBounds<'b, C2, GC2>, // constrain FS into Nova, since this is a Decider specifically for Nova Nova: From, { type ProverParam = (S::ProvingKey, CS1::ProverParams); type Proof = Proof; type VerifierParam = (S::VerifyingKey, CS1::VerifierParams); type PublicInput = Vec; type CommittedInstanceWithWitness = (); type CommittedInstance = CommittedInstance; fn prove( pp: Self::ProverParam, mut rng: impl RngCore + CryptoRng, folding_scheme: FS, ) -> Result { let (snark_pk, cs_pk): (S::ProvingKey, CS1::ProverParams) = pp; let circuit = DeciderEthCircuit::::from_nova::( folding_scheme.into(), )?; let snark_proof = S::prove(&snark_pk, circuit.clone(), &mut rng) .map_err(|e| Error::Other(e.to_string()))?; let cmT = circuit.cmT.unwrap(); let r_Fr = circuit.r.unwrap(); let W_i1 = circuit.W_i1.unwrap(); // get the challenges that have been already computed when preparing the circuit inputs in // the above `from_nova` call let challenge_W = circuit .kzg_c_W .ok_or(Error::MissingValue("kzg_c_W".to_string()))?; let challenge_E = circuit .kzg_c_E .ok_or(Error::MissingValue("kzg_c_E".to_string()))?; // generate KZG proofs let U_cmW_proof = CS1::prove_with_challenge( &cs_pk, challenge_W, &W_i1.W, &C1::ScalarField::zero(), None, )?; let U_cmE_proof = CS1::prove_with_challenge( &cs_pk, challenge_E, &W_i1.E, &C1::ScalarField::zero(), None, )?; Ok(Self::Proof { snark_proof, kzg_proofs: [U_cmW_proof, U_cmE_proof], cmT, r: r_Fr, kzg_challenges: [challenge_W, challenge_E], }) } fn verify( vp: Self::VerifierParam, i: C1::ScalarField, z_0: Vec, z_i: Vec, running_instance: &Self::CommittedInstance, incoming_instance: &Self::CommittedInstance, proof: Self::Proof, ) -> Result { let (snark_vk, cs_vk): (S::VerifyingKey, CS1::VerifierParams) = vp; // compute U = U_{d+1}= NIFS.V(U_d, u_d, cmT) let U = NIFS::::verify(proof.r, running_instance, incoming_instance, &proof.cmT); let (cmE_x, cmE_y) = point_to_nonnative_limbs_custom_opt::(U.cmE, OptimizationType::Constraints)?; let (cmW_x, cmW_y) = point_to_nonnative_limbs_custom_opt::(U.cmW, OptimizationType::Constraints)?; let (cmT_x, cmT_y) = point_to_nonnative_limbs_custom_opt::(proof.cmT, OptimizationType::Constraints)?; let public_input: Vec = vec![ vec![i], z_0, z_i, vec![U.u], U.x.clone(), cmE_x, cmE_y, cmW_x, cmW_y, proof.kzg_challenges.to_vec(), vec![ proof.kzg_proofs[0].eval, // eval_W proof.kzg_proofs[1].eval, // eval_E ], cmT_x, cmT_y, vec![proof.r], ] .concat(); let snark_v = S::verify(&snark_vk, &public_input, &proof.snark_proof) .map_err(|e| Error::Other(e.to_string()))?; if !snark_v { return Err(Error::SNARKVerificationFail); } // we're at the Ethereum EVM case, so the CS1 is KZG commitments CS1::verify_with_challenge( &cs_vk, proof.kzg_challenges[0], &U.cmW, &proof.kzg_proofs[0], )?; CS1::verify_with_challenge( &cs_vk, proof.kzg_challenges[1], &U.cmE, &proof.kzg_proofs[1], )?; Ok(true) } } #[cfg(test)] pub mod tests { use super::*; use ark_bn254::{constraints::GVar, Bn254, Fr, G1Projective as Projective}; use ark_groth16::Groth16; use ark_grumpkin::{constraints::GVar as GVar2, Projective as Projective2}; use ark_poly_commit::kzg10::VerifierKey as KZGVerifierKey; use std::time::Instant; use crate::commitment::kzg::{ProverKey as KZGProverKey, KZG}; use crate::commitment::pedersen::Pedersen; use crate::folding::nova::{get_cs_params_len, ProverParams}; use crate::frontend::tests::CubicFCircuit; use crate::transcript::poseidon::poseidon_test_config; #[test] fn test_decider() { // use Nova as FoldingScheme type NOVA = Nova< Projective, GVar, Projective2, GVar2, CubicFCircuit, KZG<'static, Bn254>, Pedersen, >; type DECIDER = Decider< Projective, GVar, Projective2, GVar2, CubicFCircuit, KZG<'static, Bn254>, Pedersen, Groth16, // here we define the Snark to use in the decider NOVA, // here we define the FoldingScheme to use >; let mut rng = ark_std::test_rng(); let poseidon_config = poseidon_test_config::(); let F_circuit = CubicFCircuit::::new(()); let z_0 = vec![Fr::from(3_u32)]; let (cs_len, cf_cs_len) = get_cs_params_len::>( &poseidon_config, F_circuit, ) .unwrap(); let start = Instant::now(); let (kzg_pk, kzg_vk): (KZGProverKey, KZGVerifierKey) = KZG::::setup(&mut rng, cs_len).unwrap(); let (cf_pedersen_params, _) = Pedersen::::setup(&mut rng, cf_cs_len).unwrap(); println!("generated KZG params, {:?}", start.elapsed()); let prover_params = ProverParams::, Pedersen> { poseidon_config: poseidon_config.clone(), cs_params: kzg_pk.clone(), cf_cs_params: cf_pedersen_params, }; let start = Instant::now(); let mut nova = NOVA::init(&prover_params, F_circuit, z_0.clone()).unwrap(); println!("Nova initialized, {:?}", start.elapsed()); let start = Instant::now(); nova.prove_step().unwrap(); println!("prove_step, {:?}", start.elapsed()); nova.prove_step().unwrap(); // do a 2nd step // generate Groth16 setup let circuit = DeciderEthCircuit::< Projective, GVar, Projective2, GVar2, KZG, Pedersen, >::from_nova::>(nova.clone()) .unwrap(); let mut rng = rand::rngs::OsRng; let start = Instant::now(); let (g16_pk, g16_vk) = Groth16::::circuit_specific_setup(circuit.clone(), &mut rng).unwrap(); println!("Groth16 setup, {:?}", start.elapsed()); // decider proof generation let start = Instant::now(); let decider_pp = (g16_pk, kzg_pk); let proof = DECIDER::prove(decider_pp, rng, nova.clone()).unwrap(); println!("Decider prove, {:?}", start.elapsed()); // decider proof verification let start = Instant::now(); let decider_vp = (g16_vk, kzg_vk); let verified = DECIDER::verify( decider_vp, nova.i, nova.z_0, nova.z_i, &nova.U_i, &nova.u_i, proof, ) .unwrap(); assert!(verified); println!("Decider verify, {:?}", start.elapsed()); } }