/// This file implements the onchain (Ethereum's EVM) decider. use ark_bn254::Bn254; use ark_crypto_primitives::sponge::Absorb; use ark_ec::{AffineRepr, CurveGroup, Group}; use ark_ff::{BigInteger, PrimeField}; use ark_groth16::Groth16; use ark_r1cs_std::{groups::GroupOpsBounds, prelude::CurveVar, ToConstraintFieldGadget}; use ark_snark::SNARK; use ark_std::rand::{CryptoRng, RngCore}; use ark_std::{One, Zero}; use core::marker::PhantomData; pub use super::decider_eth_circuit::{DeciderEthCircuit, KZGChallengesGadget}; use super::{nifs::NIFS, CommittedInstance, Nova}; use crate::commitment::{ kzg::{Proof as KZGProof, KZG}, pedersen::Params as PedersenParams, CommitmentScheme, }; use crate::folding::circuits::{nonnative::affine::NonNativeAffineVar, CF2}; 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> + ToConstraintFieldGadget>, FC: FCircuit, // CS1 is a KZG commitment, where challenge is C1::Fr elem CS1: CommitmentScheme< C1, ProverChallenge = C1::ScalarField, Challenge = C1::ScalarField, Proof = KZGProof, >, // 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 GC1: GroupOpsBounds<'b, C1, GC1>, for<'b> &'b GC2: GroupOpsBounds<'b, C2, GC2>, // constrain FS into Nova, since this is a Decider specifically for Nova Nova: From, crate::folding::nova::ProverParams: From<>::ProverParam>, crate::folding::nova::VerifierParams: From<>::VerifierParam>, { type PreprocessorParam = (FS::ProverParam, FS::VerifierParam); type ProverParam = (S::ProvingKey, CS1::ProverParams); type Proof = Proof; /// VerifierParam = (pp_hash, snark::vk, commitment_scheme::vk) type VerifierParam = (C1::ScalarField, S::VerifyingKey, CS1::VerifierParams); type PublicInput = Vec; type CommittedInstance = CommittedInstance; fn preprocess( mut rng: impl RngCore + CryptoRng, prep_param: &Self::PreprocessorParam, fs: FS, ) -> Result<(Self::ProverParam, Self::VerifierParam), Error> { let circuit = DeciderEthCircuit::::from_nova::(fs.into()).unwrap(); // get the Groth16 specific setup for the circuit let (g16_pk, g16_vk) = S::circuit_specific_setup(circuit, &mut rng).unwrap(); // get the FoldingScheme prover & verifier params from Nova #[allow(clippy::type_complexity)] let nova_pp: as FoldingScheme>::ProverParam = prep_param.0.clone().into() ; #[allow(clippy::type_complexity)] let nova_vp: as FoldingScheme>::VerifierParam = prep_param.1.clone().into(); let pp_hash = nova_vp.pp_hash()?; let pp = (g16_pk, nova_pp.cs_pp); let vp = (pp_hash, g16_vk, nova_vp.cs_vp); Ok((pp, vp)) } fn prove( mut rng: impl RngCore + CryptoRng, pp: Self::ProverParam, 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 { if i <= C1::ScalarField::one() { return Err(Error::NotEnoughSteps); } let (pp_hash, snark_vk, cs_vk): (C1::ScalarField, 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) = NonNativeAffineVar::inputize(U.cmE)?; let (cmW_x, cmW_y) = NonNativeAffineVar::inputize(U.cmW)?; let (cmT_x, cmT_y) = NonNativeAffineVar::inputize(proof.cmT)?; let public_input: Vec = vec![ vec![pp_hash, 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) } } /// Prepares solidity calldata for calling the NovaDecider contract pub fn prepare_calldata( function_signature_check: [u8; 4], i: ark_bn254::Fr, z_0: Vec, z_i: Vec, running_instance: &CommittedInstance, incoming_instance: &CommittedInstance, proof: Proof, Groth16>, ) -> Result, Error> { Ok(vec![ function_signature_check.to_vec(), i.into_bigint().to_bytes_be(), // i z_0.iter() .flat_map(|v| v.into_bigint().to_bytes_be()) .collect::>(), // z_0 z_i.iter() .flat_map(|v| v.into_bigint().to_bytes_be()) .collect::>(), // z_i point_to_eth_format(running_instance.cmW.into_affine())?, // U_i_cmW point_to_eth_format(running_instance.cmE.into_affine())?, // U_i_cmE running_instance.u.into_bigint().to_bytes_be(), // U_i_u incoming_instance.u.into_bigint().to_bytes_be(), // u_i_u proof.r.into_bigint().to_bytes_be(), // r running_instance .x .iter() .flat_map(|v| v.into_bigint().to_bytes_be()) .collect::>(), // U_i_x point_to_eth_format(incoming_instance.cmW.into_affine())?, // u_i_cmW incoming_instance .x .iter() .flat_map(|v| v.into_bigint().to_bytes_be()) .collect::>(), // u_i_x point_to_eth_format(proof.cmT.into_affine())?, // cmT point_to_eth_format(proof.snark_proof.a)?, // pA point2_to_eth_format(proof.snark_proof.b)?, // pB point_to_eth_format(proof.snark_proof.c)?, // pC proof.kzg_challenges[0].into_bigint().to_bytes_be(), // challenge_W proof.kzg_challenges[1].into_bigint().to_bytes_be(), // challenge_E proof.kzg_proofs[0].eval.into_bigint().to_bytes_be(), // eval W proof.kzg_proofs[1].eval.into_bigint().to_bytes_be(), // eval E point_to_eth_format(proof.kzg_proofs[0].proof.into_affine())?, // W kzg_proof point_to_eth_format(proof.kzg_proofs[1].proof.into_affine())?, // E kzg_proof ] .concat()) } fn point_to_eth_format(p: C) -> Result, Error> where C::BaseField: PrimeField, { // the encoding of the additive identity is [0, 0] on the EVM let zero_point = (&C::BaseField::zero(), &C::BaseField::zero()); let (x, y) = p.xy().unwrap_or(zero_point); Ok([x.into_bigint().to_bytes_be(), y.into_bigint().to_bytes_be()].concat()) } fn point2_to_eth_format(p: ark_bn254::G2Affine) -> Result, Error> { let zero_point = (&ark_bn254::Fq2::zero(), &ark_bn254::Fq2::zero()); let (x, y) = p.xy().unwrap_or(zero_point); Ok([ x.c1.into_bigint().to_bytes_be(), x.c0.into_bigint().to_bytes_be(), y.c1.into_bigint().to_bytes_be(), y.c0.into_bigint().to_bytes_be(), ] .concat()) } #[cfg(test)] pub mod tests { 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 std::time::Instant; use super::*; use crate::commitment::kzg::KZG; use crate::commitment::pedersen::Pedersen; use crate::folding::nova::PreprocessorParam; use crate::frontend::tests::CubicFCircuit; use crate::transcript::poseidon::poseidon_canonical_config; #[test] fn test_decider() { // use Nova as FoldingScheme type N = Nova< Projective, GVar, Projective2, GVar2, CubicFCircuit, KZG<'static, Bn254>, Pedersen, >; type D = Decider< Projective, GVar, Projective2, GVar2, CubicFCircuit, KZG<'static, Bn254>, Pedersen, Groth16, // here we define the Snark to use in the decider N, // here we define the FoldingScheme to use >; let mut rng = ark_std::test_rng(); let poseidon_config = poseidon_canonical_config::(); let F_circuit = CubicFCircuit::::new(()).unwrap(); let z_0 = vec![Fr::from(3_u32)]; let prep_param = PreprocessorParam::new(poseidon_config, F_circuit); let nova_params = N::preprocess(&mut rng, &prep_param).unwrap(); let start = Instant::now(); let mut nova = N::init(nova_params.clone(), F_circuit, z_0.clone()).unwrap(); println!("Nova initialized, {:?}", start.elapsed()); let start = Instant::now(); nova.prove_step(&mut rng, vec![]).unwrap(); println!("prove_step, {:?}", start.elapsed()); nova.prove_step(&mut rng, vec![]).unwrap(); // do a 2nd step let mut rng = rand::rngs::OsRng; // prepare the Decider prover & verifier params let (decider_pp, decider_vp) = D::preprocess(&mut rng, &nova_params, nova.clone()).unwrap(); // decider proof generation let start = Instant::now(); let proof = D::prove(rng, decider_pp, nova.clone()).unwrap(); println!("Decider prove, {:?}", start.elapsed()); // decider proof verification let start = Instant::now(); let verified = D::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()); } }