diff --git a/halo2_gadgets/benches/endoscale.rs b/halo2_gadgets/benches/endoscale.rs index d87b8a8cc..fcb597684 100644 --- a/halo2_gadgets/benches/endoscale.rs +++ b/halo2_gadgets/benches/endoscale.rs @@ -80,7 +80,7 @@ where config: Self::Config, mut layouter: impl Layouter, ) -> Result<(), Error> { - config.0.alg_2.table.load(&mut layouter)?; + config.0.alg_2().table.load(&mut layouter)?; let bitstring = config @@ -181,7 +181,7 @@ where config: Self::Config, mut layouter: impl Layouter, ) -> Result<(), Error> { - config.alg_2.table.load(&mut layouter)?; + config.alg_2().table.load(&mut layouter)?; let bitstring = config.witness_bitstring(&mut layouter, &self.bitstring.transpose_array(), false)?; diff --git a/halo2_gadgets/examples/endoscaling_demo.rs b/halo2_gadgets/examples/endoscaling_demo.rs index cb3c7f199..5ae47e898 100644 --- a/halo2_gadgets/examples/endoscaling_demo.rs +++ b/halo2_gadgets/examples/endoscaling_demo.rs @@ -16,17 +16,13 @@ use std::marker::PhantomData; -use ff::{Field, PrimeField}; +use ff::Field; use group::{prime::PrimeCurveAffine, Curve}; use halo2_gadgets::{ ecc::chip::NonIdentityEccPoint, - endoscale::{ - chip::{Bitstring, EndoscaleConfig}, - EndoscaleInstructions, - }, + endoscale::{chip::EndoscaleConfig, EndoscaleInstructions}, poseidon::{Pow5Chip, Pow5Config}, transcript::{chip::TranscriptChipP128Pow5T3, TranscriptInstructions}, - utilities::decompose_running_sum::RunningSumConfig, }; use halo2_proofs::{ circuit::{Layouter, Region, SimpleFloorPlanner, Value}, @@ -236,12 +232,6 @@ struct ChallengeMultiplicationCircuit { secret_input: Value, } -/// Window size for a running sum used in endoscaling's algorithm 1. -/// -/// This will always be 2! The program won't compile if it's set to anything else. -/// Algorithm 1 requires a bitstring as input that is divided into pairs. -const RUNNING_SUM_WINDOW_SIZE: usize = 2; - #[derive(Debug, Clone)] struct ChallengeMultiplicationConfig { silly_equality_config: SillyEqualityConfig, @@ -252,12 +242,6 @@ struct ChallengeMultiplicationConfig { /// Contains the expected output of the transcript squeeze (used for testing). outputs: Column, - /// Configuration for breaking down a challenge into a set of pairs of bits. - /// - First parameter is the type of the field over which the circuit operates. - /// - Second parameter defines the number of bits per window, where we intend to break the - /// input into a bitstring grouped into windows. This should always be 2 for endoscaling. - running_sum_config: RunningSumConfig, - /// Configuration for the endoscaling bit of the circuit. /// Endoscaling provides two different algorithms: Alg 1 actually computes endoscaling, and /// Alg 2 computes the scalar value from a challenge that's equivalent to replacing @@ -301,8 +285,6 @@ impl Circuit for ChallengeMultiplicationCircuit { .collect::>(); meta.enable_constant(fixeds[0]); - let selector = meta.selector(); - // Set up the configuration for poseidon + transcripts. let pow5 = Pow5Chip::configure::( meta, @@ -329,9 +311,6 @@ impl Circuit for ChallengeMultiplicationCircuit { .expect("won't fail because length is hardcoded"), ); - // Set up config for the running sum needed to configure the challenge for endoscaling - let running_sum_config = RunningSumConfig::configure(meta, selector, advices[0]); - // Set up config for endoscaling let endoscale_config = EndoscaleConfig::configure( meta, @@ -346,7 +325,6 @@ impl Circuit for ChallengeMultiplicationCircuit { silly_equality_config, pow5, outputs: instance, - running_sum_config, endoscale_config, } } @@ -426,43 +404,11 @@ impl Circuit for ChallengeMultiplicationCircuit { let challenge = chip.squeeze_challenge(layouter.namespace(|| "squeeze challenge"))?; layouter.constrain_instance(challenge.cell(), config.outputs, 0)?; - // Use the running sum gadget to convert the challenge to a bitstring - let bitstring = layouter.assign_region( - || "decompose challenge into bits", - |mut region| { - // It's necessary to strictly contstrain the running sum to be the correct size! - // Otherwise a malicious prover can construct an invalid decomposition. - // - // Future work: This section should also constrain the decomposed running sum to - // be in canonical form. Without this check, a malicious prover gets a small - // amount of freedom to choose the representation here, which we generally don't - // want to provide to them. - let strict = true; - - // Offset is 0 because we're only doing one thing in this region, the decomposition. - let offset = 0; - - // This literally refers to the number of bits in the challenge we're passing. - // It would need to be modified if you use this gadget in an arbitrary field. - let word_num_bits = pallas::Base::NUM_BITS as usize; - - // Number of windows; this refers to the number of bits in the challenge - // divided by the decomposed-window size. - let num_windows = (word_num_bits + 1) / RUNNING_SUM_WINDOW_SIZE; - - config - .running_sum_config - .copy_decompose( - &mut region, - offset, - challenge.clone(), - strict, - word_num_bits, - num_windows, - ) - .map(Bitstring::Pair) - }, - )?; + // Use the endoscale config to convert that challenge into a bitstring that can be used + // with algorithm 1. + let bitstring = config + .endoscale_config + .convert_to_bitstring(&mut layouter, &challenge)?; // Endoscale two points with algorithm 1. We used `var_base` because the "base" point -- our // `known_point` -- isn't known to both parties. diff --git a/halo2_gadgets/src/endoscale.rs b/halo2_gadgets/src/endoscale.rs index e25d6af6c..42cac081a 100644 --- a/halo2_gadgets/src/endoscale.rs +++ b/halo2_gadgets/src/endoscale.rs @@ -37,6 +37,14 @@ where for_base: bool, ) -> Result, Error>; + /// Transform an assigned cell, which is typically a random challenge generated from a + /// transcript, into a [`Self::Bitstring`] suitable for use in endoscaling. + fn convert_to_bitstring( + &self, + layouter: &mut impl Layouter, + challenge: &AssignedCell, + ) -> Result; + /// Computes commitment (Alg 1) to a variable-length bitstring using the endoscaling /// algorithm. Uses the fixed bases defined in [`Self::FixedBases`]. /// diff --git a/halo2_gadgets/src/endoscale/chip.rs b/halo2_gadgets/src/endoscale/chip.rs index cd04a8d8f..451ac7351 100644 --- a/halo2_gadgets/src/endoscale/chip.rs +++ b/halo2_gadgets/src/endoscale/chip.rs @@ -9,6 +9,7 @@ use halo2_proofs::{ plonk::{Advice, Assigned, Column, ConstraintSystem, Error, Instance}, }; use halo2curves::{pasta, pluto_eris}; +use std::iter::zip; mod alg_1; mod alg_2; @@ -17,12 +18,22 @@ use alg_1::Alg1Config; use alg_2::Alg2Config; /// Bitstring used in endoscaling. +/// +/// It's recommended to endoscale with a bitstring of length at least 160 but this is not +/// enforced by this type. #[derive(Clone, Debug)] -#[allow(clippy::type_complexity)] pub enum Bitstring { - /// TODO: docs + /// A bitstring broken into 2-bit windows for use in "algorithm 1". + /// + /// Note: the constant parameter `K` is not used with this variant; we recommend setting it to + /// 0 if the use case does not involve constructing `KBit` variants to avoid unnecessary work. Pair(alg_1::Bitstring), - /// TODO: docs + + /// A bitstring broken into `K`-bit windows for use in "algorithm 2". + /// + /// The algorithm uses a lookup table to compute the scalar for each window, then combines + /// those values into the actual "endoscalar". A larger `K` value means the lookup table will + /// be larger but reduces the cost of the recombination step. KBit(alg_2::Bitstring), } @@ -32,10 +43,15 @@ pub struct EndoscaleConfig, - /// TODO: docs - pub alg_2: Alg2Config, + /// Configuration for algorithm 1, primarily used to execute endoscaling of a curve point with + /// a challenge, encoded as a [`Bitstring::Pair`]. + alg_1: Alg1Config, + + /// Configuration for algorithm 2, primarily used to compute the "endoscalar". + /// + /// That is, given a bitstring, this computes the scalar value that, when scalar-multiplied + /// by a curve point, provides the same result as endoscaling with the bitstring. + alg_2: Alg2Config, } impl @@ -43,9 +59,17 @@ impl where C::Base: PrimeFieldBits, { + /// Get the config for algorithm 1. + pub fn alg_1(&self) -> &Alg1Config { + &self.alg_1 + } + + /// Get the config for algorithm 2 . + pub fn alg_2(&self) -> &Alg2Config { + &self.alg_2 + } + /// TODO: docs - #[allow(dead_code)] - #[allow(clippy::too_many_arguments)] pub fn configure( meta: &mut ConstraintSystem, // Advice columns not shared across alg_1 and alg_2 @@ -140,23 +164,42 @@ where .collect() } - #[allow(clippy::type_complexity)] + fn convert_to_bitstring( + &self, + layouter: &mut impl Layouter, + challenge: &AssignedCell, + ) -> Result { + // NB: This outputs a `Bitstring::Pair` variant compatible with Alg 1 only. + // There is currently no equivalent method for Alg 2. + self.alg_1 + .convert_to_bitstring( + layouter.namespace(|| "alg 1: convert challenge to bitstring"), + challenge, + ) + .map(Bitstring::Pair) + } + fn endoscale_fixed_base( &self, layouter: &mut impl Layouter, bitstring: Vec, bases: Vec, ) -> Result, Error> { - let mut points = Vec::new(); - for (bitstring, base) in bitstring.iter().zip(bases.iter()) { - match bitstring { - Bitstring::Pair(bitstring) => { - points.push(self.alg_1.endoscale_fixed_base(layouter, bitstring, base)?) - } - _ => unreachable!(), - } + if bitstring.len() != bases.len() { + Err(Error::Synthesis)? } - Ok(points) + + zip(bitstring, bases) + .map(|(bitstring, base)| match bitstring { + Bitstring::Pair(bitstring) => self + .alg_1() + .endoscale_fixed_base(layouter, &bitstring, &base), + // Endoscaling only makes sense when the input is the `Bitstring::Pair` type. + // Otherwise you've prepared the input for computing the _endoscalar_ and should + // use the `compute_endoscalar` or `constrain_bitstring` methods. + _ => Err(Error::Synthesis), + }) + .collect() } fn endoscale_var_base( @@ -165,16 +208,21 @@ where bitstring: Vec, bases: Vec, ) -> Result, Error> { - let mut points = Vec::new(); - for (bitstring, base) in bitstring.iter().zip(bases.iter()) { - match bitstring { + if bitstring.len() != bases.len() { + Err(Error::Synthesis)? + } + + zip(bitstring, bases) + .map(|(bitstring, base)| match bitstring { Bitstring::Pair(bitstring) => { - points.push(self.alg_1.endoscale_var_base(layouter, bitstring, base)?) + self.alg_1().endoscale_var_base(layouter, &bitstring, &base) } - _ => unreachable!(), - } - } - Ok(points) + // Endoscaling only makes sense when the input is the `Bitstring::Pair` type. + // Otherwise you've prepared the input for computing the _endoscalar_ and should + // use the `compute_endoscalar` or `constrain_bitstring` methods. + _ => Err(Error::Synthesis), + }) + .collect() } fn compute_endoscalar( @@ -183,8 +231,11 @@ where bitstring: &Self::Bitstring, ) -> Result, C::Base>, Error> { match bitstring { - Bitstring::KBit(bitstring) => self.alg_2.compute_endoscalar(layouter, bitstring), - _ => unreachable!(), + Bitstring::KBit(bitstring) => self.alg_2().compute_endoscalar(layouter, bitstring), + // Computing the endoscalar only makes sense when the input is the `Bitstring::Kbit` + // type. Otherwise you've prepared the input for _endoscaling_ and should use the + // `endoscale_{var | fixed}_base` methods. + _ => Err(Error::Synthesis), } } @@ -199,7 +250,10 @@ where self.alg_2 .constrain_bitstring(layouter, bitstring, pub_input_rows) } - _ => unreachable!(), + // Constraining the bitstring only makes sense when the input is the `Bitstring::Kbit` + // type. Otherwise you've prepared the input for _endoscaling_ and should use the + // `endoscale_{var | fixed}_base` methods. + _ => Err(Error::Synthesis), } } } @@ -286,7 +340,7 @@ mod tests { config: Self::Config, mut layouter: impl Layouter, ) -> Result<(), Error> { - config.0.alg_2.table.load(&mut layouter)?; + config.0.alg_2().table.load(&mut layouter)?; let bitstring = config.0.witness_bitstring( &mut layouter, @@ -389,7 +443,7 @@ mod tests { config: Self::Config, mut layouter: impl Layouter, ) -> Result<(), Error> { - config.alg_2.table.load(&mut layouter)?; + config.alg_2().table.load(&mut layouter)?; let bitstring = config.witness_bitstring( &mut layouter, diff --git a/halo2_gadgets/src/endoscale/chip/alg_1.rs b/halo2_gadgets/src/endoscale/chip/alg_1.rs index bfc2596b4..fa2d0d286 100644 --- a/halo2_gadgets/src/endoscale/chip/alg_1.rs +++ b/halo2_gadgets/src/endoscale/chip/alg_1.rs @@ -1,9 +1,9 @@ use std::iter; -use ff::{Field, PrimeFieldBits, WithSmallOrderMulGroup}; +use ff::{Field, PrimeField, PrimeFieldBits, WithSmallOrderMulGroup}; use halo2_proofs::{ arithmetic::CurveAffine, - circuit::{Layouter, Region, Value}, + circuit::{AssignedCell, Layouter, Region, Value}, plonk::{ Advice, Assigned, Column, ConstraintSystem, Constraints, Error, Expression, Selector, VirtualCells, @@ -286,6 +286,48 @@ where ) } + pub(super) fn convert_to_bitstring( + &self, + mut layouter: impl Layouter, + challenge: &AssignedCell, + ) -> Result, Error> { + // Use the running sum gadget to convert the challenge to a bitstring + layouter.assign_region( + || "decompose challenge into bits", + |mut region| { + // It's necessary to strictly constrain the running sum to be the correct size! + // Otherwise a malicious prover can construct an invalid decomposition. + // + // Future work: This section should also constrain the decomposed running sum to + // be in canonical form. Without this check, a malicious prover gets a small + // amount of freedom to choose the representation here, which we generally don't + // want to provide to them. + let strict = true; + + // Offset is 0 because we're only doing one thing in this region, the decomposition. + let offset = 0; + + // This literally refers to the number of bits in the challenge we're passing. + let word_num_bits = C::Base::NUM_BITS as usize; + + // Number of windows; this refers to the number of bits in the challenge divided + // by the decomposed-window size. + // Potential future work: we might limit this to 160, which is the folklore + // allowable size for the endoscaling bitstring + let num_windows = (word_num_bits + 1) / 2; + + self.running_sum_pairs.copy_decompose( + &mut region, + offset, + challenge.clone(), + strict, + word_num_bits, + num_windows, + ) + }, + ) + } + pub(super) fn endoscale_fixed_base( &self, layouter: &mut impl Layouter, @@ -472,7 +514,6 @@ where Ok((offset, acc)) } - #[allow(clippy::type_complexity)] fn endoscale_base_final( &self, region: &mut Region<'_, C::Base>,