//! Solidity templates for the verifier contracts. //! We use askama for templating and define which variables are required for each template. // Pragma statements for verifiers pub(crate) const PRAGMA_GROTH16_VERIFIER: &str = "pragma solidity >=0.7.0 <0.9.0;"; // from snarkjs, avoid changing pub(crate) const PRAGMA_KZG10_VERIFIER: &str = "pragma solidity >=0.8.1 <=0.8.4;"; /// Default SDPX License identifier pub(crate) const GPL3_SDPX_IDENTIFIER: &str = "// SPDX-License-Identifier: GPL-3.0"; pub(crate) const MIT_SDPX_IDENTIFIER: &str = "// SPDX-License-Identifier: MIT"; use ark_serialize::{CanonicalDeserialize, CanonicalSerialize, Read, SerializationError, Write}; mod g16; mod kzg; mod nova_cyclefold; pub use g16::Groth16Data; pub use kzg::KzgData; pub use nova_cyclefold::NovaCyclefoldData; pub trait ProtocolData: CanonicalDeserialize + CanonicalSerialize { const PROTOCOL_NAME: &'static str; fn serialize_name(&self, writer: &mut W) -> Result<(), SerializationError> { Self::PROTOCOL_NAME .to_string() .serialize_uncompressed(writer) } fn serialize_protocol_data(&self, writer: &mut W) -> Result<(), SerializationError> { self.serialize_name(writer)?; self.serialize_compressed(writer) } fn deserialize_protocol_data( mut reader: R, ) -> Result { let name: String = String::deserialize_uncompressed(&mut reader)?; let data = Self::deserialize_compressed(&mut reader)?; if name != Self::PROTOCOL_NAME { return Err(SerializationError::InvalidData); } Ok(data) } fn render_as_template(self, pragma: Option) -> Vec; } #[cfg(test)] mod tests { use crate::evm::{compile_solidity, save_solidity, Evm}; use crate::utils::HeaderInclusion; use crate::{Groth16Data, KzgData, NovaCyclefoldData, ProtocolData}; use ark_bn254::{Bn254, Fr, G1Projective as G1}; use ark_crypto_primitives::snark::{CircuitSpecificSetupSNARK, SNARK}; use ark_ec::{AffineRepr, CurveGroup}; use ark_ff::{BigInt, BigInteger, PrimeField}; use ark_groth16::Groth16; use ark_poly_commit::kzg10::VerifierKey; use ark_r1cs_std::alloc::AllocVar; use ark_r1cs_std::eq::EqGadget; use ark_r1cs_std::fields::fp::FpVar; use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError}; use ark_std::rand::{RngCore, SeedableRng}; use ark_std::Zero; use ark_std::{test_rng, UniformRand}; use askama::Template; use folding_schemes::{ commitment::{ kzg::{KZGProver, KZGSetup, ProverKey}, CommitmentProver, }, transcript::{ poseidon::{poseidon_test_config, PoseidonTranscript}, Transcript, }, }; use itertools::chain; use std::marker::PhantomData; use super::g16::Groth16Verifier; use super::kzg::KZG10Verifier; use super::nova_cyclefold::NovaCyclefoldDecider; // Function signatures for proof verification on kzg10 and groth16 contracts pub const FUNCTION_SIGNATURE_KZG10_CHECK: [u8; 4] = [0x9e, 0x78, 0xcc, 0xf7]; pub const FUNCTION_SIGNATURE_GROTH16_VERIFY_PROOF: [u8; 4] = [0x43, 0x75, 0x3b, 0x4d]; pub const FUNCTION_SIGNATURE_NOVA_CYCLEFOLD_CHECK: [u8; 4] = [0x37, 0x98, 0x0b, 0xb6]; /// Default setup length for testing. const DEFAULT_SETUP_LEN: usize = 5; #[derive(Debug, Clone, Copy)] struct TestAddCircuit { _f: PhantomData, pub x: u8, pub y: u8, pub z: u8, } impl ConstraintSynthesizer for TestAddCircuit { fn generate_constraints(self, cs: ConstraintSystemRef) -> Result<(), SynthesisError> { let x = FpVar::::new_witness(cs.clone(), || Ok(F::from(self.x)))?; let y = FpVar::::new_witness(cs.clone(), || Ok(F::from(self.y)))?; let z = FpVar::::new_input(cs.clone(), || Ok(F::from(self.z)))?; let comp_z = x.clone() + y.clone(); comp_z.enforce_equal(&z)?; Ok(()) } } #[allow(clippy::type_complexity)] fn setup<'a>( n: usize, ) -> ( ProverKey<'a, G1>, VerifierKey, ark_groth16::ProvingKey, ark_groth16::VerifyingKey, TestAddCircuit, ) { let mut rng = ark_std::rand::rngs::StdRng::seed_from_u64(test_rng().next_u64()); let (x, y, z) = (21, 21, 42); let circuit = TestAddCircuit:: { _f: PhantomData, x, y, z, }; let (g16_pk, g16_vk) = Groth16::::setup(circuit, &mut rng).unwrap(); let (kzg_pk, kzg_vk): (ProverKey, VerifierKey) = KZGSetup::::setup(&mut rng, n); (kzg_pk, kzg_vk, g16_pk, g16_vk, circuit) } #[test] fn groth16_data_serde_roundtrip() { let (_, _, _, vk, _) = setup(DEFAULT_SETUP_LEN); let g16_data = Groth16Data::from(vk); let mut bytes = vec![]; g16_data.serialize_protocol_data(&mut bytes).unwrap(); let obtained_g16_data = Groth16Data::deserialize_protocol_data(bytes.as_slice()).unwrap(); assert_eq!(g16_data, obtained_g16_data) } #[test] fn kzg_data_serde_roundtrip() { let (pk, vk, _, _, _) = setup(DEFAULT_SETUP_LEN); let kzg_data = KzgData::from((vk, Some(pk.powers_of_g[0..3].to_vec()))); let mut bytes = vec![]; kzg_data.serialize_protocol_data(&mut bytes).unwrap(); let obtained_kzg_data = KzgData::deserialize_protocol_data(bytes.as_slice()).unwrap(); assert_eq!(kzg_data, obtained_kzg_data) } #[test] fn nova_cyclefold_data_serde_roundtrip() { let (kzg_pk, kzg_vk, _, g16_vk, _) = setup(DEFAULT_SETUP_LEN); let g16_data = Groth16Data::from(g16_vk); let kzg_data = KzgData::from((kzg_vk, Some(kzg_pk.powers_of_g[0..3].to_vec()))); let mut bytes = vec![]; let nova_cyclefold_data = NovaCyclefoldData::from((g16_data, kzg_data)); nova_cyclefold_data .serialize_protocol_data(&mut bytes) .unwrap(); let obtained_nova_cyclefold_data = NovaCyclefoldData::deserialize_protocol_data(bytes.as_slice()).unwrap(); assert_eq!(nova_cyclefold_data, obtained_nova_cyclefold_data) } #[test] fn nova_cyclefold_decider_template_renders() { let (kzg_pk, kzg_vk, _, g16_vk, _) = setup(DEFAULT_SETUP_LEN); let g16_data = Groth16Data::from(g16_vk); let kzg_data = KzgData::from((kzg_vk, Some(kzg_pk.powers_of_g[0..3].to_vec()))); let nova_cyclefold_data = NovaCyclefoldData::from((g16_data, kzg_data)); let decider_template = HeaderInclusion::::builder() .template(nova_cyclefold_data) .build(); save_solidity("NovaDecider.sol", &decider_template.render().unwrap()); } #[test] fn nova_cyclefold_decider_template_compiles() { let (kzg_pk, kzg_vk, _, g16_vk, _) = setup(DEFAULT_SETUP_LEN); let g16_data = Groth16Data::from(g16_vk); let kzg_data = KzgData::from((kzg_vk, Some(kzg_pk.powers_of_g[0..3].to_vec()))); let nova_cyclefold_data = NovaCyclefoldData::from((g16_data, kzg_data)); let decider_template = HeaderInclusion::::builder() .template(nova_cyclefold_data) .build(); let decider_verifier_bytecode = compile_solidity(decider_template.render().unwrap(), "NovaDecider"); let mut evm = Evm::default(); _ = evm.create(decider_verifier_bytecode); } #[test] fn test_groth16_verifier_accepts_and_rejects_proofs() { let mut rng = ark_std::rand::rngs::StdRng::seed_from_u64(test_rng().next_u64()); let (_, _, g16_pk, g16_vk, circuit) = setup(DEFAULT_SETUP_LEN); let g16_data = Groth16Data::from(g16_vk); let proof = Groth16::::prove(&g16_pk, circuit, &mut rng).unwrap(); let res = Groth16Verifier::from(g16_data).render().unwrap(); save_solidity("groth16_verifier.sol", &res); let groth16_verifier_bytecode = compile_solidity(&res, "Verifier"); let mut evm = Evm::default(); let verifier_address = evm.create(groth16_verifier_bytecode); let (a_x, a_y) = proof.a.xy().unwrap(); let (b_x, b_y) = proof.b.xy().unwrap(); let (c_x, c_y) = proof.c.xy().unwrap(); let mut calldata: Vec = chain![ FUNCTION_SIGNATURE_GROTH16_VERIFY_PROOF, a_x.into_bigint().to_bytes_be(), a_y.into_bigint().to_bytes_be(), b_x.c1.into_bigint().to_bytes_be(), b_x.c0.into_bigint().to_bytes_be(), b_y.c1.into_bigint().to_bytes_be(), b_y.c0.into_bigint().to_bytes_be(), c_x.into_bigint().to_bytes_be(), c_y.into_bigint().to_bytes_be(), BigInt::from(Fr::from(circuit.z)).to_bytes_be(), ] .collect(); let (_, output) = evm.call(verifier_address, calldata.clone()); assert_eq!(*output.last().unwrap(), 1); // change calldata to make it invalid let last_calldata_element = calldata.last_mut().unwrap(); *last_calldata_element = 0; let (_, output) = evm.call(verifier_address, calldata); assert_eq!(*output.last().unwrap(), 0); } #[test] fn kzg_verifier_template_renders() { let (kzg_pk, kzg_vk, _, _, _) = setup(DEFAULT_SETUP_LEN); let kzg_data = KzgData::from((kzg_vk.clone(), Some(kzg_pk.powers_of_g[0..3].to_vec()))); let res = HeaderInclusion::::builder() .template(kzg_data) .build() .render() .unwrap(); // TODO: Unsure what this is testing. If we want to test correct rendering, // we should first check that it COMPLETELY renders to what we expect. assert!(res.contains(&kzg_vk.g.x.to_string())); } #[test] fn kzg_verifier_compiles() { let (kzg_pk, kzg_vk, _, _, _) = setup(DEFAULT_SETUP_LEN); let kzg_data = KzgData::from((kzg_vk.clone(), Some(kzg_pk.powers_of_g[0..3].to_vec()))); let res = HeaderInclusion::::builder() .template(kzg_data) .build() .render() .unwrap(); let kzg_verifier_bytecode = compile_solidity(res, "KZG10"); let mut evm = Evm::default(); _ = evm.create(kzg_verifier_bytecode); } #[test] fn kzg_verifier_accepts_and_rejects_proofs() { let mut rng = ark_std::rand::rngs::StdRng::seed_from_u64(test_rng().next_u64()); let poseidon_config = poseidon_test_config::(); let transcript_p = &mut PoseidonTranscript::::new(&poseidon_config); let transcript_v = &mut PoseidonTranscript::::new(&poseidon_config); let (kzg_pk, kzg_vk, _, _, _) = setup(DEFAULT_SETUP_LEN); let kzg_data = KzgData::from((kzg_vk.clone(), Some(kzg_pk.powers_of_g[0..3].to_vec()))); let v: Vec = std::iter::repeat_with(|| Fr::rand(&mut rng)) .take(DEFAULT_SETUP_LEN) .collect(); let cm = KZGProver::::commit(&kzg_pk, &v, &Fr::zero()).unwrap(); let (eval, proof) = KZGProver::::prove(&kzg_pk, transcript_p, &cm, &v, &Fr::zero(), None).unwrap(); let template = HeaderInclusion::::builder() .template(kzg_data) .build() .render() .unwrap(); let kzg_verifier_bytecode = compile_solidity(template, "KZG10"); let mut evm = Evm::default(); let verifier_address = evm.create(kzg_verifier_bytecode); let (cm_affine, proof_affine) = (cm.into_affine(), proof.into_affine()); let (x_comm, y_comm) = cm_affine.xy().unwrap(); let (x_proof, y_proof) = proof_affine.xy().unwrap(); let y = eval.into_bigint().to_bytes_be(); transcript_v.absorb_point(&cm).unwrap(); let x = transcript_v.get_challenge(); let x = x.into_bigint().to_bytes_be(); let mut calldata: Vec = chain![ FUNCTION_SIGNATURE_KZG10_CHECK, x_comm.into_bigint().to_bytes_be(), y_comm.into_bigint().to_bytes_be(), x_proof.into_bigint().to_bytes_be(), y_proof.into_bigint().to_bytes_be(), x.clone(), y, ] .collect(); let (_, output) = evm.call(verifier_address, calldata.clone()); assert_eq!(*output.last().unwrap(), 1); // change calldata to make it invalid let last_calldata_element = calldata.last_mut().unwrap(); *last_calldata_element = 0; let (_, output) = evm.call(verifier_address, calldata); assert_eq!(*output.last().unwrap(), 0); } #[test] fn nova_cyclefold_verifier_compiles() { let (kzg_pk, kzg_vk, _, g16_vk, _) = setup(DEFAULT_SETUP_LEN); let g16_data = Groth16Data::from(g16_vk); let kzg_data = KzgData::from((kzg_vk, Some(kzg_pk.powers_of_g[0..3].to_vec()))); let nova_cyclefold_data = NovaCyclefoldData::from((g16_data, kzg_data)); let decider_template = HeaderInclusion::::builder() .template(nova_cyclefold_data) .build() .render() .unwrap(); let nova_cyclefold_verifier_bytecode = compile_solidity(decider_template, "NovaDecider"); let mut evm = Evm::default(); _ = evm.create(nova_cyclefold_verifier_bytecode); } #[test] fn nova_cyclefold_verifier_accepts_and_rejects_proofs() { let mut rng = ark_std::rand::rngs::StdRng::seed_from_u64(test_rng().next_u64()); let (kzg_pk, kzg_vk, g16_pk, g16_vk, circuit) = setup(DEFAULT_SETUP_LEN); let g16_data = Groth16Data::from(g16_vk); let kzg_data = KzgData::from((kzg_vk, Some(kzg_pk.powers_of_g[0..3].to_vec()))); let nova_cyclefold_data = NovaCyclefoldData::from((g16_data, kzg_data)); let g16_proof = Groth16::::prove(&g16_pk, circuit, &mut rng).unwrap(); let (a_x, a_y) = g16_proof.a.xy().unwrap(); let (b_x, b_y) = g16_proof.b.xy().unwrap(); let (c_x, c_y) = g16_proof.c.xy().unwrap(); let poseidon_config = poseidon_test_config::(); let transcript_p = &mut PoseidonTranscript::::new(&poseidon_config); let transcript_v = &mut PoseidonTranscript::::new(&poseidon_config); let v: Vec = std::iter::repeat_with(|| Fr::rand(&mut rng)) .take(DEFAULT_SETUP_LEN) .collect(); let cm = KZGProver::::commit(&kzg_pk, &v, &Fr::zero()).unwrap(); let (eval, proof) = KZGProver::::prove(&kzg_pk, transcript_p, &cm, &v, &Fr::zero(), None).unwrap(); let decider_template = HeaderInclusion::::builder() .template(nova_cyclefold_data) .build() .render() .unwrap(); let nova_cyclefold_verifier_bytecode = compile_solidity(decider_template, "NovaDecider"); let mut evm = Evm::default(); let verifier_address = evm.create(nova_cyclefold_verifier_bytecode); let (cm_affine, proof_affine) = (cm.into_affine(), proof.into_affine()); let (x_comm, y_comm) = cm_affine.xy().unwrap(); let (x_proof, y_proof) = proof_affine.xy().unwrap(); let y = eval.into_bigint().to_bytes_be(); transcript_v.absorb_point(&cm).unwrap(); let x = transcript_v.get_challenge(); let x = x.into_bigint().to_bytes_be(); let mut calldata: Vec = chain![ FUNCTION_SIGNATURE_NOVA_CYCLEFOLD_CHECK, a_x.into_bigint().to_bytes_be(), a_y.into_bigint().to_bytes_be(), b_x.c1.into_bigint().to_bytes_be(), b_x.c0.into_bigint().to_bytes_be(), b_y.c1.into_bigint().to_bytes_be(), b_y.c0.into_bigint().to_bytes_be(), c_x.into_bigint().to_bytes_be(), c_y.into_bigint().to_bytes_be(), BigInt::from(Fr::from(circuit.z)).to_bytes_be(), x_comm.into_bigint().to_bytes_be(), y_comm.into_bigint().to_bytes_be(), x_proof.into_bigint().to_bytes_be(), y_proof.into_bigint().to_bytes_be(), x.clone(), y, ] .collect(); let (_, output) = evm.call(verifier_address, calldata.clone()); assert_eq!(*output.last().unwrap(), 1); // change calldata to make it invalid let last_calldata_element = calldata.last_mut().unwrap(); *last_calldata_element = 0; let (_, output) = evm.call(verifier_address, calldata); assert_eq!(*output.last().unwrap(), 0); } }