diff --git a/halo2_gadgets/Cargo.toml b/halo2_gadgets/Cargo.toml index 87fdd43eb..fcc94450a 100644 --- a/halo2_gadgets/Cargo.toml +++ b/halo2_gadgets/Cargo.toml @@ -92,3 +92,7 @@ harness = false [[bench]] name = "plonk" harness = false + +[[bench]] +name = "ipa_recursive_commitment" +harness = false diff --git a/halo2_gadgets/benches/ipa_recursive_commitment.rs b/halo2_gadgets/benches/ipa_recursive_commitment.rs new file mode 100644 index 000000000..d5da26364 --- /dev/null +++ b/halo2_gadgets/benches/ipa_recursive_commitment.rs @@ -0,0 +1,242 @@ +mod ecc_circuits; +mod utilities; + +use std::{iter::repeat_with, marker::PhantomData, time::Duration}; + +use criterion::{criterion_group, criterion_main, Criterion}; +use group::Curve; + +use halo2_proofs::{ + arithmetic::eval_polynomial, + circuit::{floor_planner::V1, Chip}, + generate_default_duplex_domain, + plonk::Circuit, + poly::{ + commitment::{Blind, ParamsProver}, + ipa::commitment::{create_proof, ParamsIPA}, + Coeff, EvaluationDomain, Polynomial, + }, + transcript::{ + poseidon::PoseidonWriteBaseFitsInScalarP128Pow5T3, EncodedChallenge, Transcript, + TranscriptWrite, TranscriptWriterBuffer, + }, +}; +use rand::thread_rng; + +use halo2_proofs::{ + circuit::{Layouter, Value}, + plonk::{ConstraintSystem, Error}, + poseidon::{duplex::DuplexDomain, P128Pow5T3}, +}; +use halo2curves::{pasta, CurveAffine}; +use std::{fmt::Debug, io::Read}; +use utilities::bench_circuit; + +use halo2_gadgets::{ + ecc::{ + chip::{BaseFieldElem, FixedPoint, FullScalar, ShortScalar}, + EccInstructions, FixedPoints, + }, + recursive_chip::DefaultRecursiveChip as RecursiveChip, + recursive_circuits::ipa, + transcript::{TranscriptInstructions, TranscriptReadInstructions}, +}; + +use ecc_circuits::fixed_points::TestFixedBases; + +#[derive(Debug, Clone)] +struct MyCircuit { + params: ParamsIPA, + proof: Value, + evaluation: Value<::ScalarExt>, + + _marker: PhantomData<(FP, D)>, +} + +#[allow(deprecated)] // Allow final absorb in `duplex_pattern` +mod poseidon_patterns { + use super::*; + + pub const K: u32 = 10; + + // Generated using `commit_open_pattern(10)` + generate_default_duplex_domain!( + domain, + IpaCommitOpenDomain, + F::ZERO, + 25, + DuplexPatternBuilder::new() + // Initialization: commitment and challenge + .absorb(2) + .squeeze(1) + // Open commitment + .absorb(2) + .squeeze(2) + .absorb(4) + .squeeze(1) + .absorb(4) + .squeeze(1) + .absorb(4) + .squeeze(1) + .absorb(4) + .squeeze(1) + .absorb(4) + .squeeze(1) + .absorb(4) + .squeeze(1) + .absorb(4) + .squeeze(1) + .absorb(4) + .squeeze(1) + .absorb(4) + .squeeze(1) + .absorb(4) + .squeeze(1) + .absorb(4) + .build() + ); +} + +use poseidon_patterns::{IpaCommitOpenDomain, K}; + +type C = pasta::pallas::Affine; // Limitation of `EccChip` (will be generalized later) + +impl Circuit<::Base> for MyCircuit +where + FP: FixedPoints, + >::FullScalar: FixedPoint, + >::Base: FixedPoint, + >::FullScalar: FixedPoint, + >::ShortScalar: FixedPoint, + D: DuplexDomain<::Base, { P128Pow5T3::RATE }, L>, + R: Read + Debug + Clone, +{ + type Config = as Chip<::Base>>::Config; + type FloorPlanner = V1; + #[cfg(feature = "circuit-params")] + type Params = (); + + fn without_witnesses(&self) -> Self { + Self { + params: self.params.clone(), + _marker: PhantomData, + proof: Value::unknown(), + evaluation: Value::unknown(), + } + } + + fn configure(meta: &mut ConstraintSystem<::Base>) -> Self::Config { + let advice: [_; 10] = repeat_with(|| meta.advice_column()) + .take(10) + .collect::>() + .try_into() + .unwrap(); + let fixed: [_; 8] = repeat_with(|| meta.fixed_column()) + .take(8) + .collect::>() + .try_into() + .unwrap(); + let instance = [meta.instance_column()]; + let lookup = meta.lookup_table_column(); + + RecursiveChip::configure(meta, advice, fixed, instance, lookup) + } + + fn synthesize( + &self, + config: Self::Config, + mut layouter: impl Layouter<::Base>, + ) -> Result<(), Error> { + let mut chip = RecursiveChip::init( + layouter.namespace(|| "init recursive chip"), + config, + self.proof.clone(), + )?; + + let commitment = chip.read_point(layouter.namespace(|| "commitment"))?; + let x = chip.squeeze_challenge(layouter.namespace(|| "challenge"))?; + + // `v` is the evaluation you expect: depends on the challenge but we pre-computed out + // of circuit and can just pass it in + let v = chip.witness_scalar_var(&mut layouter, self.evaluation)?; + + ipa::commitment::verify(chip, layouter, &self.params, commitment, x, v)?; + Ok(()) + } +} + +fn random_polynomial, R: rand::Rng>( + domain: &EvaluationDomain, + mut rng: R, +) -> Polynomial { + domain.coeff_from_vec( + std::iter::repeat_with(|| F::random(&mut rng)) + .take(1 << domain.k() as usize) + .collect(), + ) +} + +fn bench_recursive_ipa_commit(c: &mut Criterion) { + let params = ParamsIPA::new(K); + let mut rng = thread_rng(); + + let domain = EvaluationDomain::<::ScalarExt>::new(1, K); + + // Randomly choose a polynomial to commit to/open + let polynomial = random_polynomial(&domain, &mut rng); + let blind = Blind::new(&mut rng); + + let commitment: ::CurveExt = params.commit(&polynomial, blind); + // Create commitment opening proof to verify + let (proof, evaluation) = { + let mut transcript_write = PoseidonWriteBaseFitsInScalarP128Pow5T3::< + _, + _, + IpaCommitOpenDomain<_>, + { IpaCommitOpenDomain::<::Base>::L }, + >::init(vec![]); + + transcript_write + .write_point(commitment.to_affine()) + .expect("transcript write failed"); + let challenge_scalar = transcript_write.squeeze_challenge().get_scalar(); + + let evaluation = eval_polynomial(&polynomial, challenge_scalar); + + create_proof( + ¶ms, + rng, + &mut transcript_write, + &polynomial, + blind, + challenge_scalar, + ) + .expect("proof generation failed"); + + let proof = transcript_write.finalize(); + (std::io::Cursor::new(proof), evaluation) + }; + + let circuit = MyCircuit { + params, + proof: Value::known(proof), + evaluation: Value::known(evaluation), + _marker: PhantomData::<(TestFixedBases, IpaCommitOpenDomain<_>)>, + }; + + bench_circuit::( + c, + 13, + "IPA recursive commitment opening: degree 2^10", + &[&[]], + circuit, + ); +} + +criterion_group! { + name = benches; + config = Criterion::default().sample_size(10).measurement_time(Duration::from_secs(20)); + targets = bench_recursive_ipa_commit, +} + +criterion_main!(benches); diff --git a/halo2_gadgets/goldenfiles/cost-model/verify-ipa-commitment-circuit.csv b/halo2_gadgets/goldenfiles/cost-model/verify-ipa-commitment-circuit.csv new file mode 100644 index 000000000..862e103e6 --- /dev/null +++ b/halo2_gadgets/goldenfiles/cost-model/verify-ipa-commitment-circuit.csv @@ -0,0 +1,2 @@ +max_deg,advice_columns,lookups,permutations,column_queries,point_sets,proof_size +16,10,3,15,85,5,6464 diff --git a/halo2_gadgets/src/recursive_circuits.rs b/halo2_gadgets/src/recursive_circuits.rs index fa34855e6..738f018ac 100644 --- a/halo2_gadgets/src/recursive_circuits.rs +++ b/halo2_gadgets/src/recursive_circuits.rs @@ -1,6 +1,6 @@ //! Implementation of recrusive verification circuit subroutines -mod ipa; +pub mod ipa; mod plonk; mod recursive_gadgets; mod utils; diff --git a/halo2_gadgets/src/recursive_circuits/ipa/commitment.rs b/halo2_gadgets/src/recursive_circuits/ipa/commitment.rs index 02a29a905..bd69c6700 100644 --- a/halo2_gadgets/src/recursive_circuits/ipa/commitment.rs +++ b/halo2_gadgets/src/recursive_circuits/ipa/commitment.rs @@ -1,3 +1,15 @@ +//! This module contains an implementation of the verifier for the +//! polynomial commitment scheme described in the [Halo][halo] paper. +//! +//! +//! An equivalent non-recursive implementation (from which this recursive verification is +//! transcribed) can be found in [`halo2_proofs::poly::ipa::commitment::verify_proof`]; +//! thus it is intended to be used to recursively verify proofs created with +//! [`halo2_proofs::poly::ipa::commitment::create_proof`] +//! +//! +//! [halo]: https://eprint.iacr.org/2019/1021 + use ff::BatchInvert; use group::Curve; @@ -16,6 +28,11 @@ use crate::recursive_circuits::{ RecurseChip, RecurseCurve, }; +/// Checks an IPA proof that the polynomial behind `commitment` evaluates to `v` when queried at +/// the challenge point `x`. +/// +/// **Warning**: some scalar operations required to fully verify the proof are done out of circuit. +/// This method is insecure until deferral is fully implemented in the future. pub fn verify>( mut chip: Chip, mut layouter: impl Layouter, @@ -146,8 +163,9 @@ pub fn verify>( #[cfg(test)] mod tests { - use std::{iter::repeat_with, marker::PhantomData}; + use std::{io::Cursor, iter::repeat_with, marker::PhantomData}; + use ff::Field; use group::Curve; use halo2_proofs::{ @@ -166,7 +184,7 @@ mod tests { DuplexPatternEmitter, DuplexPatternEmitterWriteBaseFitsInScalar, }, poseidon::PoseidonWriteBaseFitsInScalarP128Pow5T3, - EncodedChallenge, Transcript, TranscriptWrite, TranscriptWriterBuffer, + EncodedChallenge, TranscriptWrite, TranscriptWriterBuffer, }, }; use rand::thread_rng; @@ -188,8 +206,10 @@ mod tests { recursive_chip::DefaultRecursiveChip as RecursiveChip, recursive_circuits::ipa::test_utils::random_polynomial, transcript::{TranscriptInstructions, TranscriptReadInstructions}, + utilities::cost_model::circuit_to_csv, }; + #[derive(Debug, Clone)] struct MyCircuit { params: ParamsIPA, proof: Value, @@ -298,106 +318,115 @@ mod tests { } } + // With `SimpleFloorPlanner`, we need no less than 2^13 rows to verify an opening + // of a degree 5 IPA polynomial commitment (including the setup for the argument). + // + // The setup involves absorbing the commitment to open and squeezing the challenge point to open at + const IPA_OPEN_K: u32 = 13; + const LOG_POLY_DEGREE: u32 = 5; + + // Verify an opening of a degree 2^LOG_POLY_DEGREE polynomial commitment with IPA #[test] fn recursive_open_valid_commit_works() { - recursive_open_commit(true); + let circuit = gen_open_commit_instance(true); + let proof = MockProver::run(IPA_OPEN_K, &circuit, vec![vec![]]).unwrap(); + proof.assert_satisfied(); } + // Verify an opening of a degree 2^LOG_POLY_DEGREE polynomial commitment with IPA + // but use a wrong evaluation: should cause the verification to fail #[test] fn recursive_open_invalid_commit_fails() { - recursive_open_commit(false); + let circuit = gen_open_commit_instance(false); + let proof = MockProver::run(IPA_OPEN_K, &circuit, vec![vec![]]).unwrap(); + assert!(proof.verify().is_err(), "invalid proof opening should fail") } - fn recursive_open_commit(valid_opening: bool) { - let k = 5; - let params = ParamsIPA::new(k); - let mut rng = thread_rng(); + #[test] + fn test_cost_model_recursive_ipa_open_commit() { + let circuit = gen_open_commit_instance(true); + + circuit_to_csv::<::Base>( + IPA_OPEN_K, + "verify-ipa-commitment-circuit", + &[&[]], + circuit, + ); + } - let domain = EvaluationDomain::<::ScalarExt>::new(1, k); + /// Produces commitment to random polynomial and populates `transcript` with proof that the + /// commitment opens to the returned value when queried at a challenge point. + fn gen_ipa_opening_proof, C: CurveAffine, E: EncodedChallenge>( + params: &ParamsIPA, + transcript: &mut T, + valid_opening: bool, + ) -> C::Scalar { + let mut rng = thread_rng(); - // Randomly choose a polynomial to commit to/open + let domain = EvaluationDomain::<::ScalarExt>::new(1, LOG_POLY_DEGREE); let polynomial = random_polynomial(&domain, &mut rng); let blind = Blind::new(&mut rng); - let commitment: ::CurveExt = params.commit(&polynomial, blind); + transcript + .write_point(commitment.to_affine()) + .expect("transcript write failed"); + let challenge_scalar = transcript.squeeze_challenge().get_scalar(); + + let mut evaluation = eval_polynomial(&polynomial, challenge_scalar); + + // This should cause the proof verification to fail + if !valid_opening { + evaluation += C::Scalar::ONE; + } + + create_proof( + params, + rng, + transcript, + &polynomial, + blind, + challenge_scalar, + ) + .expect("proof generation failed"); + + evaluation + } + + /// Generates circuit (including witness) for testing/cost-modeling + /// + /// Supplying `false` in the `valid_opening` parameter should cause the proof to fail due to an + /// invalid commitment opening + fn gen_open_commit_instance( + valid_opening: bool, + ) -> impl Circuit<::Base> + Clone { + let params = ParamsIPA::new(LOG_POLY_DEGREE); + // Create commitment opening proof to verify - let (proof, evaluation) = { - let mut transcript_write = PoseidonWriteBaseFitsInScalarP128Pow5T3::< - _, - _, - IpaCommitOpenDomain<_>, - { IpaCommitOpenDomain::<::Base>::L }, - >::init(vec![]); - - transcript_write - .write_point(commitment.to_affine()) - .expect("transcript write failed"); - let challenge_scalar = transcript_write.squeeze_challenge().get_scalar(); - - let mut evaluation = eval_polynomial(&polynomial, challenge_scalar); - - // This should cause the proof verification to fail - if !valid_opening { - evaluation += ::ScalarExt::one(); - } + let mut transcript_write = PoseidonWriteBaseFitsInScalarP128Pow5T3::< + _, + _, + IpaCommitOpenDomain<_>, + { IpaCommitOpenDomain::<::Base>::L }, + >::init(vec![]); + + let evaluation = gen_ipa_opening_proof(¶ms, &mut transcript_write, valid_opening); + let proof = Cursor::new(transcript_write.finalize()); - create_proof( - ¶ms, - rng, - &mut transcript_write, - &polynomial, - blind, - challenge_scalar, - ) - .expect("proof generation failed"); - - let proof = transcript_write.finalize(); - (std::io::Cursor::new(proof), evaluation) - }; - - let circuit = MyCircuit { + MyCircuit { params, proof: Value::known(proof), evaluation: Value::known(evaluation), _marker: PhantomData::<(TestFixedBases, IpaCommitOpenDomain<_>)>, - }; - - let proof = MockProver::run(13, &circuit, vec![vec![]]).unwrap(); - - if valid_opening { - proof.assert_satisfied(); - } else { - assert!(proof.verify().is_err(), "invalid proof opening should fail") } } // Used to generate the pattern for commitment opening fn commit_open_pattern(k: u32) { let params = ParamsIPA::new(k); - let domain = EvaluationDomain::<::ScalarExt>::new(1, k); - - let mut rng = thread_rng(); - - let polynomial = random_polynomial(&domain, &mut rng); - let commitment: ::CurveExt = - params.commit(&polynomial, Blind::new(&mut rng)); let mut transcript = DuplexPatternEmitterWriteBaseFitsInScalar::, C>::init(vec![]); - - transcript - .write_point(commitment.to_affine()) - .expect("transcript write failed"); - let challenge_scalar = transcript.squeeze_challenge().get_scalar(); - - create_proof( - ¶ms, - &mut rng, - &mut transcript, - &polynomial, - Blind::default(), - challenge_scalar, - ) - .expect("proof generation issue"); + let _evaluation = + gen_ipa_opening_proof(¶ms, &mut transcript, /* valid_opening */ true); let pattern = transcript.get_pattern(); println!("let pattern = {:?}", pattern); diff --git a/halo2_gadgets/src/recursive_circuits/ipa/multiopen.rs b/halo2_gadgets/src/recursive_circuits/ipa/multiopen.rs index e1d01422d..3ebdafcc4 100644 --- a/halo2_gadgets/src/recursive_circuits/ipa/multiopen.rs +++ b/halo2_gadgets/src/recursive_circuits/ipa/multiopen.rs @@ -1,3 +1,8 @@ +//! This module contains the verifier algorithm for the optimisation of the polynomial commitment +//! opening scheme described in the [Halo paper][halo] paper as well as the [Halo2 book][halo book] +//! +//! [halo]: https://eprint.iacr.org/2019/1021 +//! [halo book]: https://zcash.github.io/halo2/design/proving-system/multipoint-opening.html use halo2_proofs::{ arithmetic::{eval_polynomial, lagrange_interpolate}, circuit::{AssignedCell, Layouter, Value}, @@ -22,6 +27,9 @@ pub use query::VerifierQuery; /// Open multiple commited polynomials using IPA multiopen argument /// /// All query rotations are relative to `x_challenge` +/// +/// **Warning**: some scalar operations required to fully verify the proof are done out of circuit. +/// This method is insecure until deferral is fully implemented in the future. pub fn multiopen<'com, C: RecurseCurve, Chip: RecurseChip, I>( domain: &EvaluationDomain, params: &ParamsIPA,