From 5d7ed7d2926fdf58d19794483262439d1c0d9e9b Mon Sep 17 00:00:00 2001 From: Dmitry Demin Date: Tue, 2 Apr 2024 12:16:32 +0200 Subject: [PATCH] Extract zsa (optimization) related changes into separate modules (..._opt) --- halo2_gadgets/src/ecc.rs | 61 +-- halo2_gadgets/src/ecc/chip.rs | 44 +-- halo2_gadgets/src/ecc/chip/mul_fixed.rs | 6 +- halo2_gadgets/src/ecc/chip/mul_fixed/short.rs | 299 +-------------- halo2_gadgets/src/ecc/chip/witness_point.rs | 41 +-- halo2_gadgets/src/ecc_opt.rs | 61 +++ halo2_gadgets/src/ecc_opt/chip.rs | 57 +++ halo2_gadgets/src/ecc_opt/chip/mul_fixed.rs | 1 + .../src/ecc_opt/chip/mul_fixed/short.rs | 307 ++++++++++++++++ .../src/ecc_opt/chip/witness_point.rs | 50 +++ halo2_gadgets/src/lib.rs | 3 + halo2_gadgets/src/sinsemilla.rs | 123 +------ halo2_gadgets/src/sinsemilla/chip.rs | 88 +++-- .../src/sinsemilla/chip/generator_table.rs | 88 +---- .../src/sinsemilla/chip/hash_to_point.rs | 255 +++---------- halo2_gadgets/src/sinsemilla/merkle.rs | 4 +- halo2_gadgets/src/sinsemilla/merkle/chip.rs | 29 +- halo2_gadgets/src/sinsemilla/primitives.rs | 66 +--- halo2_gadgets/src/sinsemilla_opt.rs | 136 +++++++ halo2_gadgets/src/sinsemilla_opt/chip.rs | 5 + .../sinsemilla_opt/chip/generator_table.rs | 146 ++++++++ .../src/sinsemilla_opt/chip/hash_to_point.rs | 247 +++++++++++++ halo2_gadgets/src/sinsemilla_opt/merkle.rs | 3 + .../src/sinsemilla_opt/merkle/chip.rs | 59 +++ .../src/sinsemilla_opt/primitives.rs | 76 ++++ halo2_gadgets/src/utilities/cond_swap.rs | 346 +----------------- halo2_gadgets/src/utilities_opt.rs | 3 + halo2_gadgets/src/utilities_opt/cond_swap.rs | 345 +++++++++++++++++ 28 files changed, 1669 insertions(+), 1280 deletions(-) create mode 100644 halo2_gadgets/src/ecc_opt.rs create mode 100644 halo2_gadgets/src/ecc_opt/chip.rs create mode 100644 halo2_gadgets/src/ecc_opt/chip/mul_fixed.rs create mode 100644 halo2_gadgets/src/ecc_opt/chip/mul_fixed/short.rs create mode 100644 halo2_gadgets/src/ecc_opt/chip/witness_point.rs create mode 100644 halo2_gadgets/src/sinsemilla_opt.rs create mode 100644 halo2_gadgets/src/sinsemilla_opt/chip.rs create mode 100644 halo2_gadgets/src/sinsemilla_opt/chip/generator_table.rs create mode 100644 halo2_gadgets/src/sinsemilla_opt/chip/hash_to_point.rs create mode 100644 halo2_gadgets/src/sinsemilla_opt/merkle.rs create mode 100644 halo2_gadgets/src/sinsemilla_opt/merkle/chip.rs create mode 100644 halo2_gadgets/src/sinsemilla_opt/primitives.rs create mode 100644 halo2_gadgets/src/utilities_opt.rs create mode 100644 halo2_gadgets/src/utilities_opt/cond_swap.rs diff --git a/halo2_gadgets/src/ecc.rs b/halo2_gadgets/src/ecc.rs index f8da830b5f..83be238ab4 100644 --- a/halo2_gadgets/src/ecc.rs +++ b/halo2_gadgets/src/ecc.rs @@ -4,7 +4,7 @@ use std::fmt::Debug; use halo2_proofs::{ arithmetic::CurveAffine, - circuit::{AssignedCell, Chip, Layouter, Value}, + circuit::{Chip, Layouter, Value}, plonk::Error, }; @@ -60,15 +60,6 @@ pub trait EccInstructions: value: Value, ) -> Result; - /// Witnesses the given constant point as a private input to the circuit. - /// This allows the point to be the identity, mapped to (0, 0) in - /// affine coordinates. - fn witness_point_from_constant( - &self, - layouter: &mut impl Layouter, - value: C, - ) -> Result; - /// Witnesses the given point as a private input to the circuit. /// This returns an error if the point is the identity. fn witness_point_non_id( @@ -120,15 +111,6 @@ pub trait EccInstructions: b: &B, ) -> Result; - /// Performs variable-base sign-scalar multiplication, returning `[sign] point` - /// `sign` must be in {-1, 1}. - fn mul_sign( - &self, - layouter: &mut impl Layouter, - sign: &AssignedCell, - point: &Self::Point, - ) -> Result; - /// Performs variable-base scalar multiplication, returning `[scalar] base`. fn mul( &self, @@ -393,8 +375,8 @@ impl + Clone + Debug + Eq> /// A point on a specific elliptic curve. #[derive(Copy, Clone, Debug)] pub struct Point + Clone + Debug + Eq> { - chip: EccChip, - inner: EccChip::Point, + pub(crate) chip: EccChip, + pub(crate) inner: EccChip::Point, } impl + Clone + Debug + Eq> Point { @@ -408,16 +390,6 @@ impl + Clone + Debug + Eq> Point, - value: C, - ) -> Result { - let point = chip.witness_point_from_constant(&mut layouter, value); - point.map(|inner| Point { chip, inner }) - } - /// Constrains this point to be equal in value to another point. pub fn constrain_equal> + Clone>( &self, @@ -460,21 +432,6 @@ impl + Clone + Debug + Eq> Point, - sign: &AssignedCell, - ) -> Result, Error> { - self.chip - .mul_sign(&mut layouter, sign, &self.inner) - .map(|point| Point { - chip: self.chip.clone(), - inner: point, - }) - } } /// The affine short Weierstrass x-coordinate of a point on a specific elliptic curve. @@ -793,7 +750,6 @@ pub(crate) mod tests { meta.advice_column(), ]; let lookup_table = meta.lookup_table_column(); - let table_range_check_tag = meta.lookup_table_column(); let lagrange_coeffs = [ meta.fixed_column(), meta.fixed_column(), @@ -808,12 +764,7 @@ pub(crate) mod tests { let constants = meta.fixed_column(); meta.enable_constant(constants); - let range_check = LookupRangeCheckConfig::configure( - meta, - advices[9], - lookup_table, - Some(table_range_check_tag), - ); + let range_check = LookupRangeCheckConfig::configure(meta, advices[9], lookup_table); EccChip::::configure(meta, advices, lagrange_coeffs, range_check) } @@ -914,9 +865,11 @@ pub(crate) mod tests { )?; } + // FIXME: find a way to move this test outside this module as it uses optimized version + // of the chip (make chip and mul_fixed ecc_opt non-crate pub after that) // Test variable-base sign-scalar multiplication { - super::chip::mul_fixed::short::tests::test_mul_sign( + crate::ecc_opt::chip::mul_fixed::short::tests::test_mul_sign( chip.clone(), layouter.namespace(|| "variable-base sign-scalar mul"), )?; diff --git a/halo2_gadgets/src/ecc/chip.rs b/halo2_gadgets/src/ecc/chip.rs index 402020384b..acce506c5a 100644 --- a/halo2_gadgets/src/ecc/chip.rs +++ b/halo2_gadgets/src/ecc/chip.rs @@ -21,8 +21,8 @@ pub(super) mod add; pub(super) mod add_incomplete; pub mod constants; pub(super) mod mul; -pub(super) mod mul_fixed; -pub(super) mod witness_point; +pub(crate) mod mul_fixed; +pub(crate) mod witness_point; pub use constants::*; @@ -37,11 +37,11 @@ pub struct EccPoint { /// x-coordinate /// /// Stored as an `Assigned` to enable batching inversions. - x: AssignedCell, pallas::Base>, + pub(crate) x: AssignedCell, pallas::Base>, /// y-coordinate /// /// Stored as an `Assigned` to enable batching inversions. - y: AssignedCell, pallas::Base>, + pub(crate) y: AssignedCell, pallas::Base>, } impl EccPoint { @@ -153,12 +153,12 @@ pub struct EccConfig> { /// Fixed-base full-width scalar multiplication mul_fixed_full: mul_fixed::full_width::Config, /// Fixed-base signed short scalar multiplication - mul_fixed_short: mul_fixed::short::Config, + pub(crate) mul_fixed_short: mul_fixed::short::Config, /// Fixed-base mul using a base field element as a scalar mul_fixed_base_field: mul_fixed::base_field_elem::Config, /// Witness point - witness_point: witness_point::Config, + pub(crate) witness_point: witness_point::Config, /// Lookup range check using 10-bit lookup table pub lookup_config: LookupRangeCheckConfig, @@ -339,7 +339,7 @@ pub struct EccScalarFixed { type MagnitudeCell = AssignedCell; // TODO: Make V an enum Sign { Positive, Negative } type SignCell = AssignedCell; -type MagnitudeSign = (MagnitudeCell, SignCell); +pub(crate) type MagnitudeSign = (MagnitudeCell, SignCell); /// A signed short scalar used for fixed-base scalar multiplication. /// A short scalar must have magnitude in the range [0..2^64), with @@ -453,18 +453,6 @@ where ) } - fn witness_point_from_constant( - &self, - layouter: &mut impl Layouter, - value: pallas::Affine, - ) -> Result { - let config = self.config().witness_point; - layouter.assign_region( - || "witness point (constant)", - |mut region| config.constant_point(value, 0, &mut region), - ) - } - fn witness_point_non_id( &self, layouter: &mut impl Layouter, @@ -544,24 +532,6 @@ where ) } - /// Performs variable-base sign-scalar multiplication, returning `[sign] point` - /// `sign` must be in {-1, 1}. - fn mul_sign( - &self, - layouter: &mut impl Layouter, - sign: &AssignedCell, - point: &Self::Point, - ) -> Result { - // Multiply point by sign, using the same gate as mul_fixed::short. - // This also constrains sign to be in {-1, 1}. - let config_short = self.config().mul_fixed_short.clone(); - config_short.assign_scalar_sign( - layouter.namespace(|| "variable-base sign-scalar mul"), - sign, - point, - ) - } - fn mul( &self, layouter: &mut impl Layouter, diff --git a/halo2_gadgets/src/ecc/chip/mul_fixed.rs b/halo2_gadgets/src/ecc/chip/mul_fixed.rs index d0781056b8..686cb6203e 100644 --- a/halo2_gadgets/src/ecc/chip/mul_fixed.rs +++ b/halo2_gadgets/src/ecc/chip/mul_fixed.rs @@ -41,11 +41,11 @@ pub struct Config> { fixed_z: Column, // Decomposition of an `n-1`-bit scalar into `k`-bit windows: // a = a_0 + 2^k(a_1) + 2^{2k}(a_2) + ... + 2^{(n-1)k}(a_{n-1}) - window: Column, + pub(crate) window: Column, // y-coordinate of accumulator (only used in the final row). - u: Column, + pub(crate) u: Column, // Configuration for `add` - add_config: add::Config, + pub(crate) add_config: add::Config, // Configuration for `add_incomplete` add_incomplete_config: add_incomplete::Config, _marker: PhantomData, diff --git a/halo2_gadgets/src/ecc/chip/mul_fixed/short.rs b/halo2_gadgets/src/ecc/chip/mul_fixed/short.rs index 0b576d50a7..45160a23fe 100644 --- a/halo2_gadgets/src/ecc/chip/mul_fixed/short.rs +++ b/halo2_gadgets/src/ecc/chip/mul_fixed/short.rs @@ -4,7 +4,7 @@ use super::super::{EccPoint, EccScalarFixedShort, FixedPoints, L_SCALAR_SHORT, N use crate::{ecc::chip::MagnitudeSign, utilities::bool_check}; use halo2_proofs::{ - circuit::{AssignedCell, Layouter, Region}, + circuit::{Layouter, Region}, plonk::{ConstraintSystem, Constraints, Error, Expression, Selector}, poly::Rotation, }; @@ -13,8 +13,8 @@ use pasta_curves::pallas; #[derive(Clone, Debug, Eq, PartialEq)] pub struct Config> { // Selector used for fixed-base scalar mul with short signed exponent. - q_mul_fixed_short: Selector, - super_config: super::Config, + pub(crate) q_mul_fixed_short: Selector, + pub(crate) super_config: super::Config, } impl> Config { @@ -241,73 +241,11 @@ impl> Config { Ok((result, scalar)) } - - /// Multiply the point by sign, using the q_mul_fixed_short gate. - /// Constraints `sign` in {-1, 1} - pub fn assign_scalar_sign( - &self, - mut layouter: impl Layouter, - sign: &AssignedCell, - point: &EccPoint, - ) -> Result { - let signed_point = layouter.assign_region( - || "Signed point", - |mut region| { - let offset = 0; - - // Enable mul_fixed_short selector to check the sign logic. - self.q_mul_fixed_short.enable(&mut region, offset)?; - - // Set "last window" to 0 (this field is irrelevant here). - region.assign_advice_from_constant( - || "u=0", - self.super_config.u, - offset, - pallas::Base::zero(), - )?; - - // Copy sign to `window` column - sign.copy_advice(|| "sign", &mut region, self.super_config.window, offset)?; - - // Assign the input y-coordinate. - point.y.copy_advice( - || "unsigned y", - &mut region, - self.super_config.add_config.y_qr, - offset, - )?; - - // Conditionally negate y-coordinate according to the value of sign - let signed_y_val = sign.value().and_then(|sign| { - if sign == &-pallas::Base::one() { - -point.y.value() - } else { - point.y.value().cloned() - } - }); - - // Assign the output signed y-coordinate. - let signed_y = region.assign_advice( - || "signed y", - self.super_config.add_config.y_p, - offset, - || signed_y_val, - )?; - - Ok(EccPoint { - x: point.x.clone(), - y: signed_y, - }) - }, - )?; - - Ok(signed_point) - } } #[cfg(test)] pub mod tests { - use group::{ff::PrimeField, Curve, Group}; + use group::{ff::PrimeField, Curve}; use halo2_proofs::{ arithmetic::CurveAffine, circuit::{AssignedCell, Chip, Layouter, Value}, @@ -508,7 +446,6 @@ pub mod tests { meta.advice_column(), ]; let lookup_table = meta.lookup_table_column(); - let table_range_check_tag = meta.lookup_table_column(); let lagrange_coeffs = [ meta.fixed_column(), meta.fixed_column(), @@ -524,12 +461,7 @@ pub mod tests { let constants = meta.fixed_column(); meta.enable_constant(constants); - let range_check = LookupRangeCheckConfig::configure( - meta, - advices[9], - lookup_table, - Some(table_range_check_tag), - ); + let range_check = LookupRangeCheckConfig::configure(meta, advices[9], lookup_table); EccChip::::configure(meta, advices, lagrange_coeffs, range_check) } @@ -650,7 +582,7 @@ pub mod tests { )], }, VerifyFailure::Permutation { - column: (Any::Fixed, 10).into(), + column: (Any::Fixed, 9).into(), location: FailureLocation::OutsideRegion { row: 0 }, }, VerifyFailure::Permutation { @@ -725,223 +657,4 @@ pub mod tests { ); } } - - pub(crate) fn test_mul_sign( - chip: EccChip, - mut layouter: impl Layouter, - ) -> Result<(), Error> { - // Generate a random non-identity point P - let p_val = pallas::Point::random(rand::rngs::OsRng).to_affine(); - let p = Point::new( - chip.clone(), - layouter.namespace(|| "P"), - Value::known(p_val), - )?; - - // Create -P - let p_neg_val = -p_val; - let p_neg = Point::new( - chip.clone(), - layouter.namespace(|| "-P"), - Value::known(p_neg_val), - )?; - - // Create the identity point - let identity = Point::new( - chip.clone(), - layouter.namespace(|| "identity"), - Value::known(pallas::Point::identity().to_affine()), - )?; - - // Create -1 and 1 scalars - let pos_sign = chip.load_private( - layouter.namespace(|| "positive sign"), - chip.config().advices[0], - Value::known(pallas::Base::one()), - )?; - let neg_sign = chip.load_private( - layouter.namespace(|| "negative sign"), - chip.config().advices[1], - Value::known(-pallas::Base::one()), - )?; - - // [1] P == P - { - let result = p.mul_sign(layouter.namespace(|| "[1] P"), &pos_sign)?; - result.constrain_equal(layouter.namespace(|| "constrain [1] P"), &p)?; - } - - // [-1] P == -P - { - let result = p.mul_sign(layouter.namespace(|| "[1] P"), &neg_sign)?; - result.constrain_equal(layouter.namespace(|| "constrain [1] P"), &p_neg)?; - } - - // [1] 0 == 0 - { - let result = identity.mul_sign(layouter.namespace(|| "[1] O"), &pos_sign)?; - result.constrain_equal(layouter.namespace(|| "constrain [1] 0"), &identity)?; - } - - // [-1] 0 == 0 - { - let result = identity.mul_sign(layouter.namespace(|| "[-1] O"), &neg_sign)?; - result.constrain_equal(layouter.namespace(|| "constrain [1] 0"), &identity)?; - } - - Ok(()) - } - - #[test] - fn invalid_sign_in_mul_sign() { - use crate::{ecc::chip::EccConfig, utilities::UtilitiesInstructions}; - use halo2_proofs::{ - circuit::{Layouter, SimpleFloorPlanner}, - dev::{FailureLocation, MockProver, VerifyFailure}, - plonk::{Circuit, ConstraintSystem, Error}, - }; - - #[derive(Default)] - struct MyCircuit { - base: Value, - sign: Value, - } - - impl UtilitiesInstructions for MyCircuit { - type Var = AssignedCell; - } - - impl Circuit for MyCircuit { - type Config = EccConfig; - type FloorPlanner = SimpleFloorPlanner; - - fn without_witnesses(&self) -> Self { - Self::default() - } - - fn configure(meta: &mut ConstraintSystem) -> Self::Config { - let advices = [ - meta.advice_column(), - meta.advice_column(), - meta.advice_column(), - meta.advice_column(), - meta.advice_column(), - meta.advice_column(), - meta.advice_column(), - meta.advice_column(), - meta.advice_column(), - meta.advice_column(), - ]; - let lookup_table = meta.lookup_table_column(); - let table_range_check_tag = meta.lookup_table_column(); - let lagrange_coeffs = [ - meta.fixed_column(), - meta.fixed_column(), - meta.fixed_column(), - meta.fixed_column(), - meta.fixed_column(), - meta.fixed_column(), - meta.fixed_column(), - meta.fixed_column(), - ]; - - // Shared fixed column for loading constants - let constants = meta.fixed_column(); - meta.enable_constant(constants); - - let range_check = LookupRangeCheckConfig::configure( - meta, - advices[9], - lookup_table, - Some(table_range_check_tag), - ); - EccChip::::configure(meta, advices, lagrange_coeffs, range_check) - } - - fn synthesize( - &self, - config: Self::Config, - mut layouter: impl Layouter, - ) -> Result<(), Error> { - let chip = EccChip::construct(config.clone()); - - let column = config.advices[0]; - - //let short_config = config.mul_fixed_short.clone(); - let base = Point::new(chip, layouter.namespace(|| "load base"), self.base)?; - - let sign = - self.load_private(layouter.namespace(|| "load sign"), column, self.sign)?; - - base.mul_sign(layouter.namespace(|| "[sign] base"), &sign)?; - - Ok(()) - } - } - - // Copied from halo2_proofs::dev::util - fn format_value(v: pallas::Base) -> String { - use ff::Field; - if v.is_zero_vartime() { - "0".into() - } else if v == pallas::Base::one() { - "1".into() - } else if v == -pallas::Base::one() { - "-1".into() - } else { - // Format value as hex. - let s = format!("{:?}", v); - // Remove leading zeroes. - let s = s.strip_prefix("0x").unwrap(); - let s = s.trim_start_matches('0'); - format!("0x{}", s) - } - } - - // Sign that is not +/- 1 should fail - // Generate a random non-identity point - let point = pallas::Point::random(rand::rngs::OsRng); - let circuit = MyCircuit { - base: Value::known(point.to_affine()), - sign: Value::known(pallas::Base::zero()), - }; - - let prover = MockProver::::run(11, &circuit, vec![]).unwrap(); - assert_eq!( - prover.verify(), - Err(vec![ - VerifyFailure::ConstraintNotSatisfied { - constraint: ((17, "Short fixed-base mul gate").into(), 1, "sign_check").into(), - location: FailureLocation::InRegion { - region: (2, "Signed point").into(), - offset: 0, - }, - cell_values: vec![(((Any::Advice, 4).into(), 0).into(), "0".to_string())], - }, - VerifyFailure::ConstraintNotSatisfied { - constraint: ( - (17, "Short fixed-base mul gate").into(), - 3, - "negation_check" - ) - .into(), - location: FailureLocation::InRegion { - region: (2, "Signed point").into(), - offset: 0, - }, - cell_values: vec![ - ( - ((Any::Advice, 1).into(), 0).into(), - format_value(*point.to_affine().coordinates().unwrap().y()), - ), - ( - ((Any::Advice, 3).into(), 0).into(), - format_value(*point.to_affine().coordinates().unwrap().y()), - ), - (((Any::Advice, 4).into(), 0).into(), "0".to_string()), - ], - } - ]) - ); - } } diff --git a/halo2_gadgets/src/ecc/chip/witness_point.rs b/halo2_gadgets/src/ecc/chip/witness_point.rs index 98f865a6dc..580a07ca1d 100644 --- a/halo2_gadgets/src/ecc/chip/witness_point.rs +++ b/halo2_gadgets/src/ecc/chip/witness_point.rs @@ -12,14 +12,14 @@ use halo2_proofs::{ }; use pasta_curves::{arithmetic::CurveAffine, pallas}; -type Coordinates = ( +pub(crate) type Coordinates = ( AssignedCell, pallas::Base>, AssignedCell, pallas::Base>, ); #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct Config { - q_point: Selector, + pub(crate) q_point: Selector, q_point_non_id: Selector, // x-coordinate pub x: Column, @@ -102,21 +102,6 @@ impl Config { Ok((x_var, y_var)) } - fn assign_xy_from_constant( - &self, - value: (Assigned, Assigned), - offset: usize, - region: &mut Region<'_, pallas::Base>, - ) -> Result { - // Assign `x` value - let x_var = region.assign_advice_from_constant(|| "x", self.x, offset, value.0)?; - - // Assign `y` value - let y_var = region.assign_advice_from_constant(|| "y", self.y, offset, value.1)?; - - Ok((x_var, y_var)) - } - /// Assigns a point that can be the identity. pub(super) fn point( &self, @@ -141,28 +126,6 @@ impl Config { .map(|(x, y)| EccPoint::from_coordinates_unchecked(x, y)) } - /// Assigns a constant point that can be the identity. - pub(super) fn constant_point( - &self, - value: pallas::Affine, - offset: usize, - region: &mut Region<'_, pallas::Base>, - ) -> Result { - // Enable `q_point` selector - self.q_point.enable(region, offset)?; - - let value = if value == pallas::Affine::identity() { - // Map the identity to (0, 0). - (Assigned::Zero, Assigned::Zero) - } else { - let value = value.coordinates().unwrap(); - (value.x().into(), value.y().into()) - }; - - self.assign_xy_from_constant(value, offset, region) - .map(|(x, y)| EccPoint::from_coordinates_unchecked(x, y)) - } - /// Assigns a non-identity point. pub(super) fn point_non_id( &self, diff --git a/halo2_gadgets/src/ecc_opt.rs b/halo2_gadgets/src/ecc_opt.rs new file mode 100644 index 0000000000..e31d5ee4b5 --- /dev/null +++ b/halo2_gadgets/src/ecc_opt.rs @@ -0,0 +1,61 @@ +//! Elliptic curve operations. + +use std::fmt::Debug; + +use halo2_proofs::{ + arithmetic::CurveAffine, + circuit::{AssignedCell, Layouter}, + plonk::Error, +}; + +use crate::ecc::{EccInstructions, Point}; + +pub(crate) mod chip; + +/// The set of circuit instructions required to use the ECC gadgets. +pub trait EccInstructionsOptimized: EccInstructions { + /// Witnesses the given constant point as a private input to the circuit. + /// This allows the point to be the identity, mapped to (0, 0) in + /// affine coordinates. + fn witness_point_from_constant( + &self, + layouter: &mut impl Layouter, + value: C, + ) -> Result; + + /// Performs variable-base sign-scalar multiplication, returning `[sign] point` + /// `sign` must be in {-1, 1}. + fn mul_sign( + &self, + layouter: &mut impl Layouter, + sign: &AssignedCell, + point: &Self::Point, + ) -> Result; +} + +impl + Clone + Debug + Eq> Point { + /// Constructs a new point with the given fixed value. + pub fn new_from_constant( + chip: EccChip, + mut layouter: impl Layouter, + value: C, + ) -> Result { + let point = chip.witness_point_from_constant(&mut layouter, value); + point.map(|inner| Point { chip, inner }) + } + + /// Returns `[sign] self`. + /// `sign` must be in {-1, 1}. + pub fn mul_sign( + &self, + mut layouter: impl Layouter, + sign: &AssignedCell, + ) -> Result, Error> { + self.chip + .mul_sign(&mut layouter, sign, &self.inner) + .map(|point| Point { + chip: self.chip.clone(), + inner: point, + }) + } +} diff --git a/halo2_gadgets/src/ecc_opt/chip.rs b/halo2_gadgets/src/ecc_opt/chip.rs new file mode 100644 index 0000000000..566e66b503 --- /dev/null +++ b/halo2_gadgets/src/ecc_opt/chip.rs @@ -0,0 +1,57 @@ +//! Chip implementations for the ECC gadgets. + +use halo2_proofs::{ + circuit::{AssignedCell, Chip, Layouter}, + plonk::Error, +}; +use pasta_curves::pallas; + +use crate::ecc::{ + chip::{BaseFieldElem, EccChip, FixedPoint, FullScalar, ShortScalar}, + FixedPoints, +}; + +use super::EccInstructionsOptimized; + +pub(crate) mod mul_fixed; +pub(super) mod witness_point; + +impl> EccInstructionsOptimized for EccChip +where + >::Base: + FixedPoint, + >::FullScalar: + FixedPoint, + >::ShortScalar: + FixedPoint, +{ + fn witness_point_from_constant( + &self, + layouter: &mut impl Layouter, + value: pallas::Affine, + ) -> Result { + let config = self.config().witness_point; + layouter.assign_region( + || "witness point (constant)", + |mut region| config.constant_point(value, 0, &mut region), + ) + } + + /// Performs variable-base sign-scalar multiplication, returning `[sign] point` + /// `sign` must be in {-1, 1}. + fn mul_sign( + &self, + layouter: &mut impl Layouter, + sign: &AssignedCell, + point: &Self::Point, + ) -> Result { + // Multiply point by sign, using the same gate as mul_fixed::short. + // This also constrains sign to be in {-1, 1}. + let config_short = self.config().mul_fixed_short.clone(); + config_short.assign_scalar_sign( + layouter.namespace(|| "variable-base sign-scalar mul"), + sign, + point, + ) + } +} diff --git a/halo2_gadgets/src/ecc_opt/chip/mul_fixed.rs b/halo2_gadgets/src/ecc_opt/chip/mul_fixed.rs new file mode 100644 index 0000000000..d1dea74e22 --- /dev/null +++ b/halo2_gadgets/src/ecc_opt/chip/mul_fixed.rs @@ -0,0 +1 @@ +pub mod short; diff --git a/halo2_gadgets/src/ecc_opt/chip/mul_fixed/short.rs b/halo2_gadgets/src/ecc_opt/chip/mul_fixed/short.rs new file mode 100644 index 0000000000..2600582699 --- /dev/null +++ b/halo2_gadgets/src/ecc_opt/chip/mul_fixed/short.rs @@ -0,0 +1,307 @@ +use crate::ecc::chip::EccPoint; + +use super::super::FixedPoints; + +use halo2_proofs::{ + circuit::{AssignedCell, Layouter}, + plonk::Error, +}; +use pasta_curves::pallas; + +use crate::ecc::chip::mul_fixed::short::Config; + +impl> Config { + /// Multiply the point by sign, using the q_mul_fixed_short gate. + /// Constraints `sign` in {-1, 1} + pub fn assign_scalar_sign( + &self, + mut layouter: impl Layouter, + sign: &AssignedCell, + point: &EccPoint, + ) -> Result { + let signed_point = layouter.assign_region( + || "Signed point", + |mut region| { + let offset = 0; + + // Enable mul_fixed_short selector to check the sign logic. + self.q_mul_fixed_short.enable(&mut region, offset)?; + + // Set "last window" to 0 (this field is irrelevant here). + region.assign_advice_from_constant( + || "u=0", + self.super_config.u, + offset, + pallas::Base::zero(), + )?; + + // Copy sign to `window` column + sign.copy_advice(|| "sign", &mut region, self.super_config.window, offset)?; + + // Assign the input y-coordinate. + point.y.copy_advice( + || "unsigned y", + &mut region, + self.super_config.add_config.y_qr, + offset, + )?; + + // Conditionally negate y-coordinate according to the value of sign + let signed_y_val = sign.value().and_then(|sign| { + if sign == &-pallas::Base::one() { + -point.y.value() + } else { + point.y.value().cloned() + } + }); + + // Assign the output signed y-coordinate. + let signed_y = region.assign_advice( + || "signed y", + self.super_config.add_config.y_p, + offset, + || signed_y_val, + )?; + + Ok(EccPoint { + x: point.x.clone(), + y: signed_y, + }) + }, + )?; + + Ok(signed_point) + } +} + +#[cfg(test)] +pub mod tests { + use group::{Curve, Group}; + use halo2_proofs::{ + arithmetic::CurveAffine, + circuit::{AssignedCell, Chip, Layouter, Value}, + plonk::{Any, Error}, + }; + use pasta_curves::pallas; + + use crate::{ + ecc::{ + chip::{EccChip}, + tests::{TestFixedBases}, Point, + }, + utilities::{lookup_range_check::LookupRangeCheckConfig, UtilitiesInstructions}, + }; + + pub(crate) fn test_mul_sign( + chip: EccChip, + mut layouter: impl Layouter, + ) -> Result<(), Error> { + // Generate a random non-identity point P + let p_val = pallas::Point::random(rand::rngs::OsRng).to_affine(); + let p = Point::new( + chip.clone(), + layouter.namespace(|| "P"), + Value::known(p_val), + )?; + + // Create -P + let p_neg_val = -p_val; + let p_neg = Point::new( + chip.clone(), + layouter.namespace(|| "-P"), + Value::known(p_neg_val), + )?; + + // Create the identity point + let identity = Point::new( + chip.clone(), + layouter.namespace(|| "identity"), + Value::known(pallas::Point::identity().to_affine()), + )?; + + // Create -1 and 1 scalars + let pos_sign = chip.load_private( + layouter.namespace(|| "positive sign"), + chip.config().advices[0], + Value::known(pallas::Base::one()), + )?; + let neg_sign = chip.load_private( + layouter.namespace(|| "negative sign"), + chip.config().advices[1], + Value::known(-pallas::Base::one()), + )?; + + // [1] P == P + { + let result = p.mul_sign(layouter.namespace(|| "[1] P"), &pos_sign)?; + result.constrain_equal(layouter.namespace(|| "constrain [1] P"), &p)?; + } + + // [-1] P == -P + { + let result = p.mul_sign(layouter.namespace(|| "[1] P"), &neg_sign)?; + result.constrain_equal(layouter.namespace(|| "constrain [1] P"), &p_neg)?; + } + + // [1] 0 == 0 + { + let result = identity.mul_sign(layouter.namespace(|| "[1] O"), &pos_sign)?; + result.constrain_equal(layouter.namespace(|| "constrain [1] 0"), &identity)?; + } + + // [-1] 0 == 0 + { + let result = identity.mul_sign(layouter.namespace(|| "[-1] O"), &neg_sign)?; + result.constrain_equal(layouter.namespace(|| "constrain [1] 0"), &identity)?; + } + + Ok(()) + } + + #[test] + fn invalid_sign_in_mul_sign() { + use crate::{ecc::chip::EccConfig, utilities::UtilitiesInstructions}; + use halo2_proofs::{ + circuit::{Layouter, SimpleFloorPlanner}, + dev::{FailureLocation, MockProver, VerifyFailure}, + plonk::{Circuit, ConstraintSystem, Error}, + }; + + #[derive(Default)] + struct MyCircuit { + base: Value, + sign: Value, + } + + impl UtilitiesInstructions for MyCircuit { + type Var = AssignedCell; + } + + impl Circuit for MyCircuit { + type Config = EccConfig; + type FloorPlanner = SimpleFloorPlanner; + + fn without_witnesses(&self) -> Self { + Self::default() + } + + fn configure(meta: &mut ConstraintSystem) -> Self::Config { + let advices = [ + meta.advice_column(), + meta.advice_column(), + meta.advice_column(), + meta.advice_column(), + meta.advice_column(), + meta.advice_column(), + meta.advice_column(), + meta.advice_column(), + meta.advice_column(), + meta.advice_column(), + ]; + let lookup_table = meta.lookup_table_column(); + let lagrange_coeffs = [ + meta.fixed_column(), + meta.fixed_column(), + meta.fixed_column(), + meta.fixed_column(), + meta.fixed_column(), + meta.fixed_column(), + meta.fixed_column(), + meta.fixed_column(), + ]; + + // Shared fixed column for loading constants + let constants = meta.fixed_column(); + meta.enable_constant(constants); + + let range_check = LookupRangeCheckConfig::configure(meta, advices[9], lookup_table); + EccChip::::configure(meta, advices, lagrange_coeffs, range_check) + } + + fn synthesize( + &self, + config: Self::Config, + mut layouter: impl Layouter, + ) -> Result<(), Error> { + let chip = EccChip::construct(config.clone()); + + let column = config.advices[0]; + + //let short_config = config.mul_fixed_short.clone(); + let base = Point::new(chip, layouter.namespace(|| "load base"), self.base)?; + + let sign = + self.load_private(layouter.namespace(|| "load sign"), column, self.sign)?; + + base.mul_sign(layouter.namespace(|| "[sign] base"), &sign)?; + + Ok(()) + } + } + + // Copied from halo2_proofs::dev::util + fn format_value(v: pallas::Base) -> String { + use ff::Field; + if v.is_zero_vartime() { + "0".into() + } else if v == pallas::Base::one() { + "1".into() + } else if v == -pallas::Base::one() { + "-1".into() + } else { + // Format value as hex. + let s = format!("{:?}", v); + // Remove leading zeroes. + let s = s.strip_prefix("0x").unwrap(); + let s = s.trim_start_matches('0'); + format!("0x{}", s) + } + } + + // Sign that is not +/- 1 should fail + // Generate a random non-identity point + let point = pallas::Point::random(rand::rngs::OsRng); + let circuit = MyCircuit { + base: Value::known(point.to_affine()), + sign: Value::known(pallas::Base::zero()), + }; + + let prover = MockProver::::run(11, &circuit, vec![]).unwrap(); + assert_eq!( + prover.verify(), + Err(vec![ + VerifyFailure::ConstraintNotSatisfied { + constraint: ((17, "Short fixed-base mul gate").into(), 1, "sign_check").into(), + location: FailureLocation::InRegion { + region: (2, "Signed point").into(), + offset: 0, + }, + cell_values: vec![(((Any::Advice, 4).into(), 0).into(), "0".to_string())], + }, + VerifyFailure::ConstraintNotSatisfied { + constraint: ( + (17, "Short fixed-base mul gate").into(), + 3, + "negation_check" + ) + .into(), + location: FailureLocation::InRegion { + region: (2, "Signed point").into(), + offset: 0, + }, + cell_values: vec![ + ( + ((Any::Advice, 1).into(), 0).into(), + format_value(*point.to_affine().coordinates().unwrap().y()), + ), + ( + ((Any::Advice, 3).into(), 0).into(), + format_value(*point.to_affine().coordinates().unwrap().y()), + ), + (((Any::Advice, 4).into(), 0).into(), "0".to_string()), + ], + } + ]) + ); + } +} diff --git a/halo2_gadgets/src/ecc_opt/chip/witness_point.rs b/halo2_gadgets/src/ecc_opt/chip/witness_point.rs new file mode 100644 index 0000000000..9e1bfeae6d --- /dev/null +++ b/halo2_gadgets/src/ecc_opt/chip/witness_point.rs @@ -0,0 +1,50 @@ +use crate::ecc::chip::EccPoint; + +use group::prime::PrimeCurveAffine; + +use halo2_proofs::{ + circuit::Region, + plonk::{Assigned, Error}, +}; +use pasta_curves::{arithmetic::CurveAffine, pallas}; + +use crate::ecc::chip::witness_point::{Config, Coordinates}; + +impl Config { + fn assign_xy_from_constant( + &self, + value: (Assigned, Assigned), + offset: usize, + region: &mut Region<'_, pallas::Base>, + ) -> Result { + // Assign `x` value + let x_var = region.assign_advice_from_constant(|| "x", self.x, offset, value.0)?; + + // Assign `y` value + let y_var = region.assign_advice_from_constant(|| "y", self.y, offset, value.1)?; + + Ok((x_var, y_var)) + } + + /// Assigns a constant point that can be the identity. + pub(crate) fn constant_point( + &self, + value: pallas::Affine, + offset: usize, + region: &mut Region<'_, pallas::Base>, + ) -> Result { + // Enable `q_point` selector + self.q_point.enable(region, offset)?; + + let value = if value == pallas::Affine::identity() { + // Map the identity to (0, 0). + (Assigned::Zero, Assigned::Zero) + } else { + let value = value.coordinates().unwrap(); + (value.x().into(), value.y().into()) + }; + + self.assign_xy_from_constant(value, offset, region) + .map(|(x, y)| EccPoint::from_coordinates_unchecked(x, y)) + } +} diff --git a/halo2_gadgets/src/lib.rs b/halo2_gadgets/src/lib.rs index 2ac2623a99..33ffcfa6b8 100644 --- a/halo2_gadgets/src/lib.rs +++ b/halo2_gadgets/src/lib.rs @@ -22,9 +22,12 @@ #![deny(unsafe_code)] pub mod ecc; +pub mod ecc_opt; pub mod poseidon; #[cfg(feature = "unstable-sha256-gadget")] #[cfg_attr(docsrs, doc(cfg(feature = "unstable-sha256-gadget")))] pub mod sha256; pub mod sinsemilla; +pub mod sinsemilla_opt; pub mod utilities; +pub mod utilities_opt; diff --git a/halo2_gadgets/src/sinsemilla.rs b/halo2_gadgets/src/sinsemilla.rs index 0cfd821e98..f96ea57079 100644 --- a/halo2_gadgets/src/sinsemilla.rs +++ b/halo2_gadgets/src/sinsemilla.rs @@ -78,7 +78,7 @@ pub trait SinsemillaInstructions Result<(Self::NonIdentityPoint, Vec), Error>; - /// Hashes a message to an ECC curve point. - /// This returns both the resulting point, as well as the message - /// decomposition in the form of intermediate values in a cumulative - /// sum. - /// The initial point `Q` is a private point. - #[allow(non_snake_case)] - #[allow(clippy::type_complexity)] - fn hash_to_point_with_private_init( - &self, - layouter: impl Layouter, - Q: &Self::NonIdentityPoint, - message: Self::Message, - ) -> Result<(Self::NonIdentityPoint, Vec), Error>; - /// Extracts the x-coordinate of the output of a Sinsemilla hash. fn extract(point: &Self::NonIdentityPoint) -> Self::X; } @@ -116,8 +102,8 @@ pub struct Message + Clone + Debug + Eq, { - chip: SinsemillaChip, - inner: SinsemillaChip::Message, + pub(crate) chip: SinsemillaChip, + pub(crate) inner: SinsemillaChip::Message, } impl @@ -297,9 +283,9 @@ pub struct HashDomain< + Debug + Eq, { - sinsemilla_chip: SinsemillaChip, - ecc_chip: EccChip, - Q: C, + pub(crate) sinsemilla_chip: SinsemillaChip, + pub(crate) ecc_chip: EccChip, + pub(crate) Q: C, } impl @@ -343,21 +329,6 @@ where .map(|(point, zs)| (ecc::NonIdentityPoint::from_inner(self.ecc_chip.clone(), point), zs)) } - #[allow(non_snake_case)] - #[allow(clippy::type_complexity)] - /// Evaluate the Sinsemilla hash of `message` from the private initial point `Q`. - pub fn hash_to_point_with_private_init( - &self, - layouter: impl Layouter, - Q: &>::NonIdentityPoint, - message: Message, - ) -> Result<(ecc::NonIdentityPoint, Vec), Error> { - assert_eq!(self.sinsemilla_chip, message.chip); - self.sinsemilla_chip - .hash_to_point_with_private_init(layouter, Q, message.inner) - .map(|(point, zs)| (ecc::NonIdentityPoint::from_inner(self.ecc_chip.clone(), point), zs)) - } - /// $\mathsf{SinsemillaHash}$ from [§ 5.4.1.9][concretesinsemillahash]. /// /// [concretesinsemillahash]: https://zips.z.cash/protocol/protocol.pdf#concretesinsemillahash @@ -412,8 +383,8 @@ pub struct CommitDomain< + Debug + Eq, { - M: HashDomain, - R: ecc::FixedPoint, + pub(crate) M: HashDomain, + pub(crate) R: ecc::FixedPoint, } impl @@ -441,63 +412,6 @@ where } } - #[allow(clippy::type_complexity)] - /// Evaluates the Sinsemilla hash of `message` from the public initial point `Q` stored - /// into `CommitDomain`. - pub fn hash( - &self, - layouter: impl Layouter, - message: Message, - ) -> Result< - ( - ecc::NonIdentityPoint, - Vec, - ), - Error, - > { - assert_eq!(self.M.sinsemilla_chip, message.chip); - self.M.hash_to_point(layouter, message) - } - - #[allow(non_snake_case)] - #[allow(clippy::type_complexity)] - /// Evaluates the Sinsemilla hash of `message` from the private initial point `Q`. - pub fn hash_with_private_init( - &self, - layouter: impl Layouter, - Q: &>::NonIdentityPoint, - message: Message, - ) -> Result< - ( - ecc::NonIdentityPoint, - Vec, - ), - Error, - > { - assert_eq!(self.M.sinsemilla_chip, message.chip); - self.M.hash_to_point_with_private_init(layouter, Q, message) - } - - #[allow(clippy::type_complexity)] - /// Returns the public initial point `Q` stored into `CommitDomain`. - pub fn q_init(&self) -> C { - self.M.Q - } - - #[allow(clippy::type_complexity)] - /// Evaluates the blinding factor equal to $\[r\] R$ where `r` is stored in the `CommitDomain`. - pub fn blinding_factor( - &self, - mut layouter: impl Layouter, - r: ecc::ScalarFixed, - ) -> Result< - ecc::Point, - Error, - > { - let (blind, _) = self.R.mul(layouter.namespace(|| "[r] R"), r)?; - Ok(blind) - } - #[allow(clippy::type_complexity)] /// $\mathsf{SinsemillaCommit}$ from [§ 5.4.8.4][concretesinsemillacommit]. /// @@ -516,11 +430,15 @@ where > { assert_eq!(self.M.sinsemilla_chip, message.chip); - // FIXME: it's not a breaking change because `blinding_factor` simply wraps `R.mul` + // FIXME: consider returning ZSA version of the following lines. + // It's not a breaking change because `blinding_factor` simply wraps `R.mul` // and `hash` simply wraps `M.hash_to_point` - are those wrapper really needed? - let blind = self.blinding_factor(layouter.namespace(|| "[r] R"), r)?; - let (p, zs) = self.hash(layouter.namespace(|| "M"), message)?; - + //let blind = self.blinding_factor(layouter.namespace(|| "[r] R"), r)?; + //let (p, zs) = self.hash(layouter.namespace(|| "M"), message)?; + + let (blind, _) = self.R.mul(layouter.namespace(|| "[r] R"), r)?; + let (p, zs) = self.M.hash_to_point(layouter.namespace(|| "M"), message)?; + let commitment = p.add(layouter.namespace(|| "M + [r] R"), &blind)?; Ok((commitment, zs)) } @@ -641,7 +559,6 @@ pub(crate) mod tests { meta.enable_constant(constants); let table_idx = meta.lookup_table_column(); - let table_range_check_tag = meta.lookup_table_column(); let lagrange_coeffs = [ meta.fixed_column(), meta.fixed_column(), @@ -658,15 +575,9 @@ pub(crate) mod tests { table_idx, meta.lookup_table_column(), meta.lookup_table_column(), - Some(table_range_check_tag), ); - let range_check = LookupRangeCheckConfig::configure( - meta, - advices[9], - table_idx, - Some(table_range_check_tag), - ); + let range_check = LookupRangeCheckConfig::configure(meta, advices[9], table_idx); let ecc_config = EccChip::::configure(meta, advices, lagrange_coeffs, range_check); diff --git a/halo2_gadgets/src/sinsemilla/chip.rs b/halo2_gadgets/src/sinsemilla/chip.rs index 022941dd82..40177b7201 100644 --- a/halo2_gadgets/src/sinsemilla/chip.rs +++ b/halo2_gadgets/src/sinsemilla/chip.rs @@ -9,6 +9,7 @@ use crate::{ chip::{DoubleAndAdd, NonIdentityEccPoint}, FixedPoints, }, + sinsemilla_opt::SinsemillaInstructionsOptimized, utilities::lookup_range_check::LookupRangeCheckConfig, }; use std::marker::PhantomData; @@ -23,10 +24,12 @@ use halo2_proofs::{ }; use pasta_curves::pallas; -mod generator_table; +pub(crate) mod generator_table; use generator_table::GeneratorTableConfig; -mod hash_to_point; +use crate::sinsemilla_opt::chip::generator_table::GeneratorTableConfigOptimized; + +pub(crate) mod hash_to_point; /// Configuration for the Sinsemilla hash chip #[derive(Eq, PartialEq, Clone, Debug)] @@ -44,11 +47,11 @@ where /// q_sinsemilla3 = (q_sinsemilla2) ⋅ (q_sinsemilla2 - 1) /// Simple selector used to constrain hash initialization to be consistent with /// the y-coordinate of the domain $Q$. - q_sinsemilla4: Selector, + pub(crate) q_sinsemilla4: Selector, /// Fixed column used to load the y-coordinate of the domain $Q$. fixed_y_q: Column, /// Logic specific to merged double-and-add. - double_and_add: DoubleAndAdd, + pub(crate) double_and_add: DoubleAndAdd, /// Advice column used to load the message. bits: Column, /// Advice column used to witness message pieces. This may or may not be the same @@ -56,11 +59,9 @@ where witness_pieces: Column, /// The lookup table where $(\mathsf{idx}, x_p, y_p)$ are loaded for the $2^K$ /// generators of the Sinsemilla hash. - pub(super) generator_table: GeneratorTableConfig, + pub(crate) generator_table_optimized: GeneratorTableConfigOptimized, /// An advice column configured to perform lookup range checks. lookup_config: LookupRangeCheckConfig, - /// FIXME: add a proper comment - is_zsa_variant: bool, _marker: PhantomData<(Hash, Commit, F)>, } @@ -92,6 +93,10 @@ where let q_s2 = meta.query_fixed(self.q_sinsemilla2); q_s2.clone() * (q_s2 - one) } + + fn generator_table(&self) -> &GeneratorTableConfig { + &self.generator_table_optimized.inner + } } /// A chip that implements 10-bit Sinsemilla using a lookup table and 5 advice columns. @@ -142,7 +147,32 @@ where layouter: &mut impl Layouter, ) -> Result<>::Loaded, Error> { // Load the lookup table. - config.generator_table.load(layouter) + config.generator_table_optimized.load(layouter) + } + + /// FIXME: add doc + pub fn configure( + meta: &mut ConstraintSystem, + advices: [Column; 5], + witness_pieces: Column, + fixed_y_q: Column, + lookup: (TableColumn, TableColumn, TableColumn), + range_check: LookupRangeCheckConfig, + ) -> >::Config { + let lookup = ( + lookup.0, + lookup.1, + lookup.2, + range_check.zsa.map(|zsa| zsa.table_range_check_tag), + ); + Self::configure_with_tag( + meta, + advices, + witness_pieces, + fixed_y_q, + lookup, + range_check, + ) } /// # Side-effects @@ -150,7 +180,7 @@ where /// All columns in `advices` and will be equality-enabled. #[allow(clippy::too_many_arguments)] #[allow(non_snake_case)] - pub fn configure( + pub fn configure_with_tag( meta: &mut ConstraintSystem, advices: [Column; 5], witness_pieces: Column, @@ -176,20 +206,20 @@ where }, bits: advices[2], witness_pieces, - generator_table: GeneratorTableConfig { - table_idx: lookup.0, - table_x: lookup.1, - table_y: lookup.2, + generator_table_optimized: GeneratorTableConfigOptimized { + inner: GeneratorTableConfig { + table_idx: lookup.0, + table_x: lookup.1, + table_y: lookup.2, + }, table_range_check_tag: lookup.3, }, lookup_config: range_check, - // FIXME: consider passing is_zsa_enabled to `configure` function explicitly - is_zsa_variant: lookup.3.is_some(), _marker: PhantomData, }; // Set up lookup argument - GeneratorTableConfig::configure(meta, config.clone()); + GeneratorTableConfigOptimized::configure(meta, config.clone()); let two = pallas::Base::from(2); @@ -209,7 +239,11 @@ where meta.create_gate("Initial y_Q", |meta| { let q_s4 = meta.query_selector(config.q_sinsemilla4); - let y_q = if config.is_zsa_variant { + let y_q = if config + .generator_table_optimized + .table_range_check_tag + .is_some() + { meta.query_advice(config.double_and_add.x_p, Rotation::prev()) } else { meta.query_fixed(config.fixed_y_q) @@ -327,10 +361,24 @@ where ) -> Result<(Self::NonIdentityPoint, Vec), Error> { layouter.assign_region( || "hash_to_point", - |mut region| self.hash_message(&mut region, Q, &message), + |mut region| self.hash_message_optimized(&mut region, Q, &message), ) } + fn extract(point: &Self::NonIdentityPoint) -> Self::X { + point.x() + } +} + +// Implement `SinsemillaInstructions` for `SinsemillaChip` +impl + SinsemillaInstructionsOptimized + for SinsemillaChip +where + Hash: HashDomains, + F: FixedPoints, + Commit: CommitDomains, +{ #[allow(non_snake_case)] #[allow(clippy::type_complexity)] fn hash_to_point_with_private_init( @@ -344,8 +392,4 @@ where |mut region| self.hash_message_with_private_init(&mut region, Q, &message), ) } - - fn extract(point: &Self::NonIdentityPoint) -> Self::X { - point.x() - } } diff --git a/halo2_gadgets/src/sinsemilla/chip/generator_table.rs b/halo2_gadgets/src/sinsemilla/chip/generator_table.rs index 3d8943d5e7..3d70ec531a 100644 --- a/halo2_gadgets/src/sinsemilla/chip/generator_table.rs +++ b/halo2_gadgets/src/sinsemilla/chip/generator_table.rs @@ -6,7 +6,7 @@ use halo2_proofs::{ }; use super::{CommitDomains, FixedPoints, HashDomains}; -use crate::sinsemilla::primitives::{self as sinsemilla, K, SINSEMILLA_S}; +use crate::sinsemilla::primitives::{self as sinsemilla, SINSEMILLA_S}; use pasta_curves::pallas; /// Table containing independent generators S[0..2^k] @@ -15,7 +15,6 @@ pub struct GeneratorTableConfig { pub table_idx: TableColumn, pub table_x: TableColumn, pub table_y: TableColumn, - pub table_range_check_tag: Option, } impl GeneratorTableConfig { @@ -33,9 +32,9 @@ impl GeneratorTableConfig { Commit: CommitDomains, { let (table_idx, table_x, table_y) = ( - config.generator_table.table_idx, - config.generator_table.table_x, - config.generator_table.table_y, + config.generator_table().table_idx, + config.generator_table().table_x, + config.generator_table().table_y, ); // https://p.z.cash/halo2-0.1:sinsemilla-constraints?partial @@ -78,22 +77,6 @@ impl GeneratorTableConfig { }); } - /// Load the generator table into the circuit. - /// - /// | table_idx | table_x | table_y | table_range_check_tag | - /// ------------------------------------------------------------------- - /// | 0 | X(S\[0\]) | Y(S\[0\]) | 0 | - /// | 1 | X(S\[1\]) | Y(S\[1\]) | 0 | - /// | ... | ... | ... | 0 | - /// | 2^10-1 | X(S\[2^10-1\]) | Y(S\[2^10-1\]) | 0 | - /// | 0 | X(S\[0\]) | Y(S\[0\]) | 4 | - /// | 1 | X(S\[1\]) | Y(S\[1\]) | 4 | - /// | ... | ... | ... | 4 | - /// | 2^4-1 | X(S\[2^4-1\]) | Y(S\[2^4-1\]) | 4 | - /// | 0 | X(S\[0\]) | Y(S\[0\]) | 5 | - /// | 1 | X(S\[1\]) | Y(S\[1\]) | 5 | - /// | ... | ... | ... | 5 | - /// | 2^5-1 | X(S\[2^5-1\]) | Y(S\[2^5-1\]) | 5 | pub fn load(&self, layouter: &mut impl Layouter) -> Result<(), Error> { layouter.assign_table( || "generator_table", @@ -107,69 +90,6 @@ impl GeneratorTableConfig { )?; table.assign_cell(|| "table_x", self.table_x, index, || Value::known(*x))?; table.assign_cell(|| "table_y", self.table_y, index, || Value::known(*y))?; - - if let Some(table_range_check_tag) = self.table_range_check_tag { - table.assign_cell( - || "table_range_check_tag", - table_range_check_tag, - index, - || Value::known(pallas::Base::zero()), - )?; - if index < (1 << 4) { - let new_index = index + (1 << K); - table.assign_cell( - || "table_idx", - self.table_idx, - new_index, - || Value::known(pallas::Base::from(index as u64)), - )?; - table.assign_cell( - || "table_x", - self.table_x, - new_index, - || Value::known(*x), - )?; - table.assign_cell( - || "table_y", - self.table_y, - new_index, - || Value::known(*y), - )?; - table.assign_cell( - || "table_range_check_tag", - table_range_check_tag, - new_index, - || Value::known(pallas::Base::from(4_u64)), - )?; - } - if index < (1 << 5) { - let new_index = index + (1 << 10) + (1 << 4); - table.assign_cell( - || "table_idx", - self.table_idx, - new_index, - || Value::known(pallas::Base::from(index as u64)), - )?; - table.assign_cell( - || "table_x", - self.table_x, - new_index, - || Value::known(*x), - )?; - table.assign_cell( - || "table_y", - self.table_y, - new_index, - || Value::known(*y), - )?; - table.assign_cell( - || "table_range_check_tag", - table_range_check_tag, - new_index, - || Value::known(pallas::Base::from(5_u64)), - )?; - } - } } Ok(()) }, diff --git a/halo2_gadgets/src/sinsemilla/chip/hash_to_point.rs b/halo2_gadgets/src/sinsemilla/chip/hash_to_point.rs index c9c4bdb470..d85b468bd3 100644 --- a/halo2_gadgets/src/sinsemilla/chip/hash_to_point.rs +++ b/halo2_gadgets/src/sinsemilla/chip/hash_to_point.rs @@ -41,52 +41,12 @@ where ), Error, > { - let (offset, x_a, y_a) = if self.config.is_zsa_variant { - self.public_initialization_zsa(region, Q)? - } else { - self.public_initialization(region, Q)? - }; + let (offset, x_a, y_a) = self.public_initialization(region, Q)?; let (x_a, y_a, zs_sum) = self.hash_all_pieces(region, offset, message, x_a, y_a)?; #[cfg(test)] - #[allow(non_snake_case)] - // Check equivalence to result from primitives::sinsemilla::hash_to_point - { - use crate::sinsemilla::primitives::{K, S_PERSONALIZATION}; - - use group::{prime::PrimeCurveAffine, Curve}; - use pasta_curves::arithmetic::CurveExt; - - let field_elems: Value> = message - .iter() - .map(|piece| piece.field_elem().map(|elem| (elem, piece.num_words()))) - .collect(); - - field_elems - .zip(x_a.value().zip(y_a.value())) - .assert_if_known(|(field_elems, (x_a, y_a))| { - // Get message as a bitstring. - let bitstring: Vec = field_elems - .iter() - .flat_map(|(elem, num_words)| { - elem.to_le_bits().into_iter().take(K * num_words) - }) - .collect(); - - let hasher_S = pallas::Point::hash_to_curve(S_PERSONALIZATION); - let S = |chunk: &[bool]| hasher_S(&lebs2ip_k(chunk).to_le_bytes()); - - // We can use complete addition here because it differs from - // incomplete addition with negligible probability. - let expected_point = bitstring - .chunks(K) - .fold(Q.to_curve(), |acc, chunk| (acc + S(chunk)) + acc); - let actual_point = - pallas::Affine::from_xy(x_a.evaluate(), y_a.evaluate()).unwrap(); - expected_point.to_affine() == actual_point - }); - } + Self::check_hash_result(&Q, message, &x_a, &y_a); x_a.value() .zip(y_a.value()) @@ -97,93 +57,53 @@ where )) } - /// [Specification](https://p.z.cash/halo2-0.1:sinsemilla-constraints?partial). + #[cfg(test)] #[allow(non_snake_case)] - #[allow(clippy::type_complexity)] - pub(super) fn hash_message_with_private_init( - &self, - region: &mut Region<'_, pallas::Base>, - Q: &NonIdentityEccPoint, + // Check equivalence to result from primitives::sinsemilla::hash_to_point + pub(crate) fn check_hash_result( + Q: &pallas::Affine, message: &>::Message, - ) -> Result< - ( - NonIdentityEccPoint, - Vec>>, - ), - Error, - > { - let (offset, x_a, y_a) = self.private_initialization(region, Q)?; - - let (x_a, y_a, zs_sum) = self.hash_all_pieces(region, offset, message, x_a, y_a)?; - - // FIXME: try to avoid duplication with a very similar code block in `hash_message` method - // - it's basically the same code except the following lines: - // - // hash_message_with_private_init: - // ... - // .zip(Q.point()) - // .assert_if_known(|((field_elems, (x_a, y_a)), Q)| { - // ... - // - // hash_message: - // ... - // .assert_if_known(|(field_elems, (x_a, y_a))| { - // ... - #[cfg(test)] - #[allow(non_snake_case)] - // Check equivalence to result from primitives::sinsemilla::hash_to_point - { - use crate::sinsemilla::primitives::{K, S_PERSONALIZATION}; - - use group::{prime::PrimeCurveAffine, Curve}; - use pasta_curves::arithmetic::CurveExt; - - let field_elems: Value> = message - .iter() - .map(|piece| piece.field_elem().map(|elem| (elem, piece.num_words()))) - .collect(); - - field_elems - .zip(x_a.value().zip(y_a.value())) - .zip(Q.point()) - .assert_if_known(|((field_elems, (x_a, y_a)), Q)| { - // Get message as a bitstring. - let bitstring: Vec = field_elems - .iter() - .flat_map(|(elem, num_words)| { - elem.to_le_bits().into_iter().take(K * num_words) - }) - .collect(); - - let hasher_S = pallas::Point::hash_to_curve(S_PERSONALIZATION); - let S = |chunk: &[bool]| hasher_S(&lebs2ip_k(chunk).to_le_bytes()); - - // We can use complete addition here because it differs from - // incomplete addition with negligible probability. - let expected_point = bitstring - .chunks(K) - .fold(Q.to_curve(), |acc, chunk| (acc + S(chunk)) + acc); - let actual_point = - pallas::Affine::from_xy(x_a.evaluate(), y_a.evaluate()).unwrap(); - expected_point.to_affine() == actual_point - }); - } - - x_a.value() - .zip(y_a.value()) - .error_if_known_and(|(x_a, y_a)| x_a.is_zero_vartime() || y_a.is_zero_vartime())?; - Ok(( - NonIdentityEccPoint::from_coordinates_unchecked(x_a.0, y_a), - zs_sum, - )) + x_a: &X, + y_a: &AssignedCell, pallas::Base>, + ) { + use crate::sinsemilla::primitives::{K, S_PERSONALIZATION}; + + use group::{prime::PrimeCurveAffine, Curve}; + use pasta_curves::arithmetic::CurveExt; + + let field_elems: Value> = message + .iter() + .map(|piece| piece.field_elem().map(|elem| (elem, piece.num_words()))) + .collect(); + + field_elems + .zip(x_a.value().zip(y_a.value())) + .assert_if_known(|(field_elems, (x_a, y_a))| { + // Get message as a bitstring. + let bitstring: Vec = field_elems + .iter() + .flat_map(|(elem, num_words)| elem.to_le_bits().into_iter().take(K * num_words)) + .collect(); + + let hasher_S = pallas::Point::hash_to_curve(S_PERSONALIZATION); + let S = |chunk: &[bool]| hasher_S(&lebs2ip_k(chunk).to_le_bytes()); + + // We can use complete addition here because it differs from + // incomplete addition with negligible probability. + let expected_point = bitstring + .chunks(K) + .fold(Q.to_curve(), |acc, chunk| (acc + S(chunk)) + acc); + let actual_point = pallas::Affine::from_xy(x_a.evaluate(), y_a.evaluate()).unwrap(); + expected_point.to_affine() == actual_point + }); } #[allow(non_snake_case)] - fn public_initialization( + pub(crate) fn public_initialization( &self, region: &mut Region<'_, pallas::Base>, Q: pallas::Affine, @@ -225,98 +145,9 @@ where Ok((offset, x_a, y_a)) } - #[allow(non_snake_case)] - /// Assign the coordinates of the initial public point `Q` - /// - /// | offset | x_A | x_P | q_sinsemilla4 | - /// -------------------------------------- - /// | 0 | | y_Q | | - /// | 1 | x_Q | | 1 | - fn public_initialization_zsa( - &self, - region: &mut Region<'_, pallas::Base>, - Q: pallas::Affine, - ) -> Result<(usize, X, Y), Error> { - let config = self.config().clone(); - let mut offset = 0; - - // Get the `x`- and `y`-coordinates of the starting `Q` base. - let x_q = *Q.coordinates().unwrap().x(); - let y_q = *Q.coordinates().unwrap().y(); - - // Constrain the initial x_a, lambda_1, lambda_2, x_p using the q_sinsemilla4 - // selector. - let y_a: Y = { - // Enable `q_sinsemilla4` on the second row. - config.q_sinsemilla4.enable(region, offset + 1)?; - let y_a: AssignedCell, pallas::Base> = region - .assign_advice_from_constant( - || "fixed y_q", - config.double_and_add.x_p, - offset, - y_q.into(), - )?; - - y_a.value_field().into() - }; - offset += 1; - - // Constrain the initial x_q to equal the x-coordinate of the domain's `Q`. - let x_a: X = { - let x_a = region.assign_advice_from_constant( - || "fixed x_q", - config.double_and_add.x_a, - offset, - x_q.into(), - )?; - - x_a.into() - }; - - Ok((offset, x_a, y_a)) - } - - #[allow(non_snake_case)] - /// Assign the coordinates of the initial private point `Q` - /// - /// | offset | x_A | x_P | q_sinsemilla4 | - /// -------------------------------------- - /// | 0 | | y_Q | | - /// | 1 | x_Q | | 1 | - fn private_initialization( - &self, - region: &mut Region<'_, pallas::Base>, - Q: &NonIdentityEccPoint, - ) -> Result<(usize, X, Y), Error> { - let config = self.config().clone(); - let mut offset = 0; - - // Assign `x_Q` and `y_Q` in the region and constrain the initial x_a, lambda_1, lambda_2, - // x_p, y_Q using the q_sinsemilla4 selector. - let y_a: Y = { - // Enable `q_sinsemilla4` on the second row. - config.q_sinsemilla4.enable(region, offset + 1)?; - let q_y: AssignedCell, pallas::Base> = Q.y().into(); - let y_a: AssignedCell, pallas::Base> = - q_y.copy_advice(|| "fixed y_q", region, config.double_and_add.x_p, offset)?; - - y_a.value_field().into() - }; - offset += 1; - - let x_a: X = { - let q_x: AssignedCell, pallas::Base> = Q.x().into(); - let x_a = q_x.copy_advice(|| "fixed x_q", region, config.double_and_add.x_a, offset)?; - - x_a.into() - }; - - Ok((offset, x_a, y_a)) - } - #[allow(clippy::type_complexity)] /// Hash `message` from the initial point `Q`. - fn hash_all_pieces( + pub(crate) fn hash_all_pieces( &self, region: &mut Region<'_, pallas::Base>, mut offset: usize, @@ -594,7 +425,7 @@ where } /// The x-coordinate of the accumulator in a Sinsemilla hash instance. -struct X(AssignedCell, F>); +pub(crate) struct X(pub(crate) AssignedCell, F>); impl From, F>> for X { fn from(cell_value: AssignedCell, F>) -> Self { @@ -615,7 +446,7 @@ impl Deref for X { /// This is never actually witnessed until the last round, since it /// can be derived from other variables. Thus it only exists as a field /// element, not a `CellValue`. -struct Y(Value>); +pub(crate) struct Y(pub(crate) Value>); impl From>> for Y { fn from(value: Value>) -> Self { diff --git a/halo2_gadgets/src/sinsemilla/merkle.rs b/halo2_gadgets/src/sinsemilla/merkle.rs index c9c088f16b..02a3bdaf7c 100644 --- a/halo2_gadgets/src/sinsemilla/merkle.rs +++ b/halo2_gadgets/src/sinsemilla/merkle.rs @@ -246,11 +246,9 @@ pub mod tests { meta.lookup_table_column(), meta.lookup_table_column(), meta.lookup_table_column(), - Some(meta.lookup_table_column()), ); - let range_check = - LookupRangeCheckConfig::configure(meta, advices[9], lookup.0, lookup.3); + let range_check = LookupRangeCheckConfig::configure(meta, advices[9], lookup.0); let sinsemilla_config_1 = SinsemillaChip::configure( meta, diff --git a/halo2_gadgets/src/sinsemilla/merkle/chip.rs b/halo2_gadgets/src/sinsemilla/merkle/chip.rs index cb3c5be4cc..b9207abe36 100644 --- a/halo2_gadgets/src/sinsemilla/merkle/chip.rs +++ b/halo2_gadgets/src/sinsemilla/merkle/chip.rs @@ -36,8 +36,8 @@ where { advices: [Column; 5], q_decompose: Selector, - pub(super) cond_swap_config: CondSwapConfig, - pub(super) sinsemilla_config: SinsemillaConfig, + pub(crate) cond_swap_config: CondSwapConfig, + pub(crate) sinsemilla_config: SinsemillaConfig, } /// Chip implementing `MerkleInstructions`. @@ -441,18 +441,6 @@ where let chip = CondSwapChip::::construct(config); chip.swap(layouter, pair, swap) } - - fn mux( - &self, - layouter: &mut impl Layouter, - choice: Self::Var, - left: Self::Var, - right: Self::Var, - ) -> Result { - let config = self.config().cond_swap_config.clone(); - let chip = CondSwapChip::::construct(config); - chip.mux(layouter, choice, left, right) - } } impl SinsemillaInstructions @@ -535,19 +523,6 @@ where chip.hash_to_point(layouter, Q, message) } - #[allow(non_snake_case)] - #[allow(clippy::type_complexity)] - fn hash_to_point_with_private_init( - &self, - layouter: impl Layouter, - Q: &Self::NonIdentityPoint, - message: Self::Message, - ) -> Result<(Self::NonIdentityPoint, Vec>), Error> { - let config = self.config().sinsemilla_config.clone(); - let chip = SinsemillaChip::::construct(config); - chip.hash_to_point_with_private_init(layouter, Q, message) - } - fn extract(point: &Self::NonIdentityPoint) -> Self::X { SinsemillaChip::::extract(point) } diff --git a/halo2_gadgets/src/sinsemilla/primitives.rs b/halo2_gadgets/src/sinsemilla/primitives.rs index ad9e397b5e..2ebde8faf4 100644 --- a/halo2_gadgets/src/sinsemilla/primitives.rs +++ b/halo2_gadgets/src/sinsemilla/primitives.rs @@ -56,7 +56,7 @@ fn extract_p_bottom(point: CtOption) -> CtOption { /// Pads the given iterator (which MUST have length $\leq K * C$) with zero-bits to a /// multiple of $K$ bits. -struct Pad> { +pub(crate) struct Pad> { /// The iterator we are padding. inner: I, /// The measured length of the inner iterator. @@ -184,9 +184,10 @@ impl HashDomain { #[derive(Debug)] #[allow(non_snake_case)] pub struct CommitDomain { + // FIXME: THis comment came from ZSA version /// A domain in which $\mathsf{SinsemillaHashToPoint}$ and $\mathsf{SinsemillaHash}$ can be used - M: HashDomain, - R: pallas::Point, + pub(crate) M: HashDomain, + pub(crate) R: pallas::Point, } impl CommitDomain { @@ -201,17 +202,6 @@ impl CommitDomain { } } - /// Constructs a new `CommitDomain` from different values for `hash_domain` and `blind_domain` - pub fn new_with_personalization(hash_domain: &str, blind_domain: &str) -> Self { - let m_prefix = format!("{}-M", hash_domain); - let r_prefix = format!("{}-r", blind_domain); - let hasher_r = pallas::Point::hash_to_curve(&r_prefix); - CommitDomain { - M: HashDomain::new(&m_prefix), - R: hasher_r(&[]), - } - } - /// $\mathsf{SinsemillaCommit}$ from [§ 5.4.8.4][concretesinsemillacommit]. /// /// [concretesinsemillacommit]: https://zips.z.cash/protocol/nu5.pdf#concretesinsemillacommit @@ -226,26 +216,6 @@ impl CommitDomain { .map(|p| p + Wnaf::new().scalar(r).base(self.R)) } - /// $\mathsf{SinsemillaHashToPoint}$ from [§ 5.4.1.9][concretesinsemillahash]. - /// - /// [concretesinsemillahash]: https://zips.z.cash/protocol/nu5.pdf#concretesinsemillahash - pub fn hash_to_point(&self, msg: impl Iterator) -> CtOption { - self.M.hash_to_point(msg) - } - - /// Returns `SinsemillaCommit_r(personalization, msg) = hash_point + [r]R` - /// where `SinsemillaHash(personalization, msg) = hash_point` - /// and `R` is derived from the `personalization`. - #[allow(non_snake_case)] - pub fn commit_from_hash_point( - &self, - hash_point: CtOption, - r: &pallas::Scalar, - ) -> CtOption { - // We use complete addition for the blinding factor. - hash_point.map(|p| p + Wnaf::new().scalar(r).base(self.R)) - } - /// $\mathsf{SinsemillaShortCommit}$ from [§ 5.4.8.4][concretesinsemillacommit]. /// /// [concretesinsemillacommit]: https://zips.z.cash/protocol/nu5.pdf#concretesinsemillacommit @@ -337,32 +307,4 @@ mod tests { assert_eq!(computed, actual); } } - - #[test] - fn commit_in_several_steps() { - use rand::{rngs::OsRng, Rng}; - - use ff::Field; - - use crate::sinsemilla::primitives::CommitDomain; - - let domain = CommitDomain::new("z.cash:ZSA-NoteCommit"); - - let mut os_rng = OsRng::default(); - let msg: Vec = (0..36).map(|_| os_rng.gen::()).collect(); - - let rcm = pallas::Scalar::random(&mut os_rng); - - // Evaluate the commitment with commit function - let commit1 = domain.commit(msg.clone().into_iter(), &rcm); - - // Evaluate the commitment with the following steps - // 1. hash msg - // 2. evaluate the commitment from the hash point - let hash_point = domain.M.hash_to_point(msg.into_iter()); - let commit2 = domain.commit_from_hash_point(hash_point, &rcm); - - // Test equality - assert_eq!(commit1.unwrap(), commit2.unwrap()); - } } diff --git a/halo2_gadgets/src/sinsemilla_opt.rs b/halo2_gadgets/src/sinsemilla_opt.rs new file mode 100644 index 0000000000..14a86344a7 --- /dev/null +++ b/halo2_gadgets/src/sinsemilla_opt.rs @@ -0,0 +1,136 @@ +//! The [Sinsemilla] hash function. +//! +//! [Sinsemilla]: https://zips.z.cash/protocol/protocol.pdf#concretesinsemillahash + +use std::fmt::Debug; + +use pasta_curves::arithmetic::CurveAffine; + +use halo2_proofs::{circuit::Layouter, plonk::Error}; + +use crate::{ + ecc::{self, EccInstructions}, + sinsemilla::{CommitDomain, HashDomain, Message, SinsemillaInstructions}, +}; + +pub mod chip; +pub mod merkle; +pub mod primitives; + +/// FIXME: add a doc +pub trait SinsemillaInstructionsOptimized: + SinsemillaInstructions +{ + /// Hashes a message to an ECC curve point. + /// This returns both the resulting point, as well as the message + /// decomposition in the form of intermediate values in a cumulative + /// sum. + /// The initial point `Q` is a private point. + #[allow(non_snake_case)] + #[allow(clippy::type_complexity)] + fn hash_to_point_with_private_init( + &self, + layouter: impl Layouter, + Q: &Self::NonIdentityPoint, + message: Self::Message, + ) -> Result<(Self::NonIdentityPoint, Vec), Error>; +} + +impl + HashDomain +where + SinsemillaChip: SinsemillaInstructionsOptimized + Clone + Debug + Eq, + EccChip: EccInstructions< + C, + NonIdentityPoint = >::NonIdentityPoint, + FixedPoints = >::FixedPoints, + > + Clone + + Debug + + Eq, +{ + #[allow(non_snake_case)] + #[allow(clippy::type_complexity)] + /// Evaluate the Sinsemilla hash of `message` from the private initial point `Q`. + pub fn hash_to_point_with_private_init( + &self, + layouter: impl Layouter, + Q: &>::NonIdentityPoint, + message: Message, + ) -> Result<(ecc::NonIdentityPoint, Vec), Error> { + assert_eq!(self.sinsemilla_chip, message.chip); + self.sinsemilla_chip + .hash_to_point_with_private_init(layouter, Q, message.inner) + .map(|(point, zs)| (ecc::NonIdentityPoint::from_inner(self.ecc_chip.clone(), point), zs)) + } + +} + +impl + CommitDomain +where + SinsemillaChip: SinsemillaInstructionsOptimized + Clone + Debug + Eq, + EccChip: EccInstructions< + C, + NonIdentityPoint = >::NonIdentityPoint, + FixedPoints = >::FixedPoints, + > + Clone + + Debug + + Eq, +{ + #[allow(clippy::type_complexity)] + /// Evaluates the Sinsemilla hash of `message` from the public initial point `Q` stored + /// into `CommitDomain`. + pub fn hash( + &self, + layouter: impl Layouter, + message: Message, + ) -> Result< + ( + ecc::NonIdentityPoint, + Vec, + ), + Error, + > { + assert_eq!(self.M.sinsemilla_chip, message.chip); + self.M.hash_to_point(layouter, message) + } + + #[allow(non_snake_case)] + #[allow(clippy::type_complexity)] + /// Evaluates the Sinsemilla hash of `message` from the private initial point `Q`. + pub fn hash_with_private_init( + &self, + layouter: impl Layouter, + Q: &>::NonIdentityPoint, + message: Message, + ) -> Result< + ( + ecc::NonIdentityPoint, + Vec, + ), + Error, + > { + assert_eq!(self.M.sinsemilla_chip, message.chip); + self.M.hash_to_point_with_private_init(layouter, Q, message) + } + + #[allow(clippy::type_complexity)] + /// Returns the public initial point `Q` stored into `CommitDomain`. + pub fn q_init(&self) -> C { + self.M.Q + } + + #[allow(clippy::type_complexity)] + /// Evaluates the blinding factor equal to $\[r\] R$ where `r` is stored in the `CommitDomain`. + pub fn blinding_factor( + &self, + mut layouter: impl Layouter, + r: ecc::ScalarFixed, + ) -> Result< + ecc::Point, + Error, + > { + let (blind, _) = self.R.mul(layouter.namespace(|| "[r] R"), r)?; + Ok(blind) + } +} diff --git a/halo2_gadgets/src/sinsemilla_opt/chip.rs b/halo2_gadgets/src/sinsemilla_opt/chip.rs new file mode 100644 index 0000000000..d9ee82a4b0 --- /dev/null +++ b/halo2_gadgets/src/sinsemilla_opt/chip.rs @@ -0,0 +1,5 @@ +//! Chip implementations for the Sinsemilla gadgets. + +pub(crate) mod generator_table; + +mod hash_to_point; diff --git a/halo2_gadgets/src/sinsemilla_opt/chip/generator_table.rs b/halo2_gadgets/src/sinsemilla_opt/chip/generator_table.rs new file mode 100644 index 0000000000..3d68f99dda --- /dev/null +++ b/halo2_gadgets/src/sinsemilla_opt/chip/generator_table.rs @@ -0,0 +1,146 @@ +use halo2_proofs::{ + circuit::{Layouter, Value}, + plonk::{ConstraintSystem, Error, TableColumn}, +}; + +use pasta_curves::pallas; + +use crate::{ + ecc::FixedPoints, + sinsemilla::{ + chip::{generator_table::GeneratorTableConfig, SinsemillaConfig}, + primitives::{K, SINSEMILLA_S}, + CommitDomains, HashDomains, + }, +}; + +/// Table containing independent generators S[0..2^k] +#[derive(Eq, PartialEq, Copy, Clone, Debug)] +pub struct GeneratorTableConfigOptimized { + pub inner: GeneratorTableConfig, + pub table_range_check_tag: Option, +} + +impl GeneratorTableConfigOptimized { + /// Even though the lookup table can be used in other parts of the circuit, + /// this specific configuration sets up Sinsemilla-specific constraints + /// controlled by `q_sinsemilla`, and would likely not apply to other chips. + pub fn configure( + meta: &mut ConstraintSystem, + config: SinsemillaConfig, + ) where + Hash: HashDomains, + F: FixedPoints, + Commit: CommitDomains, + { + GeneratorTableConfig::configure(meta, config) + } + + /// Load the generator table into the circuit. + /// + /// | table_idx | table_x | table_y | table_range_check_tag | + /// ------------------------------------------------------------------- + /// | 0 | X(S\[0\]) | Y(S\[0\]) | 0 | + /// | 1 | X(S\[1\]) | Y(S\[1\]) | 0 | + /// | ... | ... | ... | 0 | + /// | 2^10-1 | X(S\[2^10-1\]) | Y(S\[2^10-1\]) | 0 | + /// | 0 | X(S\[0\]) | Y(S\[0\]) | 4 | + /// | 1 | X(S\[1\]) | Y(S\[1\]) | 4 | + /// | ... | ... | ... | 4 | + /// | 2^4-1 | X(S\[2^4-1\]) | Y(S\[2^4-1\]) | 4 | + /// | 0 | X(S\[0\]) | Y(S\[0\]) | 5 | + /// | 1 | X(S\[1\]) | Y(S\[1\]) | 5 | + /// | ... | ... | ... | 5 | + /// | 2^5-1 | X(S\[2^5-1\]) | Y(S\[2^5-1\]) | 5 | + pub fn load(&self, layouter: &mut impl Layouter) -> Result<(), Error> { + layouter.assign_table( + || "generator_table", + |mut table| { + for (index, (x, y)) in SINSEMILLA_S.iter().enumerate() { + table.assign_cell( + || "table_idx", + self.inner.table_idx, + index, + || Value::known(pallas::Base::from(index as u64)), + )?; + table.assign_cell( + || "table_x", + self.inner.table_x, + index, + || Value::known(*x), + )?; + table.assign_cell( + || "table_y", + self.inner.table_y, + index, + || Value::known(*y), + )?; + + if let Some(table_range_check_tag) = self.table_range_check_tag { + table.assign_cell( + || "table_range_check_tag", + table_range_check_tag, + index, + || Value::known(pallas::Base::zero()), + )?; + if index < (1 << 4) { + let new_index = index + (1 << K); + table.assign_cell( + || "table_idx", + self.inner.table_idx, + new_index, + || Value::known(pallas::Base::from(index as u64)), + )?; + table.assign_cell( + || "table_x", + self.inner.table_x, + new_index, + || Value::known(*x), + )?; + table.assign_cell( + || "table_y", + self.inner.table_y, + new_index, + || Value::known(*y), + )?; + table.assign_cell( + || "table_range_check_tag", + table_range_check_tag, + new_index, + || Value::known(pallas::Base::from(4_u64)), + )?; + } + if index < (1 << 5) { + let new_index = index + (1 << 10) + (1 << 4); + table.assign_cell( + || "table_idx", + self.inner.table_idx, + new_index, + || Value::known(pallas::Base::from(index as u64)), + )?; + table.assign_cell( + || "table_x", + self.inner.table_x, + new_index, + || Value::known(*x), + )?; + table.assign_cell( + || "table_y", + self.inner.table_y, + new_index, + || Value::known(*y), + )?; + table.assign_cell( + || "table_range_check_tag", + table_range_check_tag, + new_index, + || Value::known(pallas::Base::from(5_u64)), + )?; + } + } + } + Ok(()) + }, + ) + } +} diff --git a/halo2_gadgets/src/sinsemilla_opt/chip/hash_to_point.rs b/halo2_gadgets/src/sinsemilla_opt/chip/hash_to_point.rs new file mode 100644 index 0000000000..3f653db9f7 --- /dev/null +++ b/halo2_gadgets/src/sinsemilla_opt/chip/hash_to_point.rs @@ -0,0 +1,247 @@ +use pasta_curves::{arithmetic::CurveAffine, pallas}; + +use halo2_proofs::{ + circuit::{AssignedCell, Chip, Region}, + plonk::{Assigned, Error}, +}; + +use crate::{ + ecc::{chip::NonIdentityEccPoint, FixedPoints}, + sinsemilla::{ + chip::{ + hash_to_point::{X, Y}, + SinsemillaChip, + }, + primitives::{self as sinsemilla}, + CommitDomains, HashDomains, SinsemillaInstructions, + }, +}; + +impl SinsemillaChip +where + Hash: HashDomains, + Fixed: FixedPoints, + Commit: CommitDomains, +{ + /// [Specification](https://p.z.cash/halo2-0.1:sinsemilla-constraints?partial). + #[allow(non_snake_case)] + #[allow(clippy::type_complexity)] + pub(crate) fn hash_message_optimized( + &self, + region: &mut Region<'_, pallas::Base>, + Q: pallas::Affine, + message: &>::Message, + ) -> Result< + ( + NonIdentityEccPoint, + Vec>>, + ), + Error, + > { + let (offset, x_a, y_a) = if self + .config() + .generator_table_optimized + .table_range_check_tag + .is_some() + { + self.public_initialization_optimized(region, Q)? + } else { + self.public_initialization(region, Q)? + }; + + let (x_a, y_a, zs_sum) = self.hash_all_pieces(region, offset, message, x_a, y_a)?; + + #[cfg(test)] + Self::check_hash_result(&Q, message, &x_a, &y_a); + + x_a.value() + .zip(y_a.value()) + .error_if_known_and(|(x_a, y_a)| x_a.is_zero_vartime() || y_a.is_zero_vartime())?; + Ok(( + NonIdentityEccPoint::from_coordinates_unchecked(x_a.0, y_a), + zs_sum, + )) + } + + /// [Specification](https://p.z.cash/halo2-0.1:sinsemilla-constraints?partial). + #[allow(non_snake_case)] + #[allow(clippy::type_complexity)] + pub(crate) fn hash_message_with_private_init( + &self, + region: &mut Region<'_, pallas::Base>, + Q: &NonIdentityEccPoint, + message: &>::Message, + ) -> Result< + ( + NonIdentityEccPoint, + Vec>>, + ), + Error, + > { + let (offset, x_a, y_a) = self.private_initialization(region, Q)?; + + let (x_a, y_a, zs_sum) = self.hash_all_pieces(region, offset, message, x_a, y_a)?; + + // FIXME: try to avoid duplication with `check_hash_result` method + // - it's basically the same code except the following lines: + // + // hash_message_with_private_init: + // ... + // .zip(Q.point()) + // .assert_if_known(|((field_elems, (x_a, y_a)), Q)| { + // ... + // + // check_hash_result: + // ... + // .assert_if_known(|(field_elems, (x_a, y_a))| { + // ... + #[cfg(test)] + #[allow(non_snake_case)] + // Check equivalence to result from primitives::sinsemilla::hash_to_point + { + use group::{prime::PrimeCurveAffine, Curve}; + use pasta_curves::arithmetic::CurveExt; + + use ff::PrimeFieldBits; + + use halo2_proofs::circuit::Value; + + use crate::sinsemilla::primitives::{lebs2ip_k, K, S_PERSONALIZATION}; + + let field_elems: Value> = message + .iter() + .map(|piece| piece.field_elem().map(|elem| (elem, piece.num_words()))) + .collect(); + + field_elems + .zip(x_a.value().zip(y_a.value())) + .zip(Q.point()) + .assert_if_known(|((field_elems, (x_a, y_a)), Q)| { + // Get message as a bitstring. + let bitstring: Vec = field_elems + .iter() + .flat_map(|(elem, num_words)| { + elem.to_le_bits().into_iter().take(K * num_words) + }) + .collect(); + + let hasher_S = pallas::Point::hash_to_curve(S_PERSONALIZATION); + let S = |chunk: &[bool]| hasher_S(&lebs2ip_k(chunk).to_le_bytes()); + + // We can use complete addition here because it differs from + // incomplete addition with negligible probability. + let expected_point = bitstring + .chunks(K) + .fold(Q.to_curve(), |acc, chunk| (acc + S(chunk)) + acc); + let actual_point = + pallas::Affine::from_xy(x_a.evaluate(), y_a.evaluate()).unwrap(); + expected_point.to_affine() == actual_point + }); + } + + x_a.value() + .zip(y_a.value()) + .error_if_known_and(|(x_a, y_a)| x_a.is_zero_vartime() || y_a.is_zero_vartime())?; + Ok(( + NonIdentityEccPoint::from_coordinates_unchecked(x_a.0, y_a), + zs_sum, + )) + } + + #[allow(non_snake_case)] + /// Assign the coordinates of the initial public point `Q` + /// + /// | offset | x_A | x_P | q_sinsemilla4 | + /// -------------------------------------- + /// | 0 | | y_Q | | + /// | 1 | x_Q | | 1 | + fn public_initialization_optimized( + &self, + region: &mut Region<'_, pallas::Base>, + Q: pallas::Affine, + ) -> Result<(usize, X, Y), Error> { + let config = self.config().clone(); + let mut offset = 0; + + // Get the `x`- and `y`-coordinates of the starting `Q` base. + let x_q = *Q.coordinates().unwrap().x(); + let y_q = *Q.coordinates().unwrap().y(); + + // Constrain the initial x_a, lambda_1, lambda_2, x_p using the q_sinsemilla4 + // selector. + let y_a: Y = { + // Enable `q_sinsemilla4` on the second row. + config.q_sinsemilla4.enable(region, offset + 1)?; + let y_a: AssignedCell, pallas::Base> = region + .assign_advice_from_constant( + || "fixed y_q", + config.double_and_add.x_p, + offset, + y_q.into(), + )?; + + y_a.value_field().into() + }; + offset += 1; + + // Constrain the initial x_q to equal the x-coordinate of the domain's `Q`. + let x_a: X = { + let x_a = region.assign_advice_from_constant( + || "fixed x_q", + config.double_and_add.x_a, + offset, + x_q.into(), + )?; + + x_a.into() + }; + + Ok((offset, x_a, y_a)) + } + + #[allow(non_snake_case)] + /// Assign the coordinates of the initial private point `Q` + /// + /// | offset | x_A | x_P | q_sinsemilla4 | + /// -------------------------------------- + /// | 0 | | y_Q | | + /// | 1 | x_Q | | 1 | + fn private_initialization( + &self, + region: &mut Region<'_, pallas::Base>, + Q: &NonIdentityEccPoint, + ) -> Result<(usize, X, Y), Error> { + let config = self.config().clone(); + let mut offset = 0; + + // Assign `x_Q` and `y_Q` in the region and constrain the initial x_a, lambda_1, lambda_2, + // x_p, y_Q using the q_sinsemilla4 selector. + let y_a: Y = { + // Enable `q_sinsemilla4` on the second row. + config.q_sinsemilla4.enable(region, offset + 1)?; + let q_y: AssignedCell, pallas::Base> = Q.y().into(); + let y_a: AssignedCell, pallas::Base> = + q_y.copy_advice(|| "fixed y_q", region, config.double_and_add.x_p, offset)?; + + y_a.value_field().into() + }; + offset += 1; + + let x_a: X = { + let q_x: AssignedCell, pallas::Base> = Q.x().into(); + let x_a = q_x.copy_advice(|| "fixed x_q", region, config.double_and_add.x_a, offset)?; + + x_a.into() + }; + + Ok((offset, x_a, y_a)) + } +} diff --git a/halo2_gadgets/src/sinsemilla_opt/merkle.rs b/halo2_gadgets/src/sinsemilla_opt/merkle.rs new file mode 100644 index 0000000000..2927585229 --- /dev/null +++ b/halo2_gadgets/src/sinsemilla_opt/merkle.rs @@ -0,0 +1,3 @@ +//! Gadgets for implementing a Merkle tree with Sinsemilla. + +pub mod chip; diff --git a/halo2_gadgets/src/sinsemilla_opt/merkle/chip.rs b/halo2_gadgets/src/sinsemilla_opt/merkle/chip.rs new file mode 100644 index 0000000000..d692cedf44 --- /dev/null +++ b/halo2_gadgets/src/sinsemilla_opt/merkle/chip.rs @@ -0,0 +1,59 @@ +//! Chip implementing a Merkle hash using Sinsemilla as the hash function. + +use halo2_proofs::{ + circuit::{Chip, Layouter}, + plonk::Error, +}; +use pasta_curves::pallas; + +use crate::{ + sinsemilla::{merkle::chip::MerkleChip, primitives as sinsemilla}, + sinsemilla_opt::SinsemillaInstructionsOptimized, + { + ecc::FixedPoints, + sinsemilla::{chip::SinsemillaChip, CommitDomains, HashDomains}, + utilities::cond_swap::CondSwapChip, + utilities_opt::cond_swap::CondSwapInstructionsOptimized, + }, +}; + +impl CondSwapInstructionsOptimized for MerkleChip +where + Hash: HashDomains, + F: FixedPoints, + Commit: CommitDomains, +{ + fn mux( + &self, + layouter: &mut impl Layouter, + choice: Self::Var, + left: Self::Var, + right: Self::Var, + ) -> Result { + let config = self.config().cond_swap_config.clone(); + let chip = CondSwapChip::::construct(config); + chip.mux(layouter, choice, left, right) + } +} + +impl + SinsemillaInstructionsOptimized + for MerkleChip +where + Hash: HashDomains, + F: FixedPoints, + Commit: CommitDomains, +{ + #[allow(non_snake_case)] + #[allow(clippy::type_complexity)] + fn hash_to_point_with_private_init( + &self, + layouter: impl Layouter, + Q: &Self::NonIdentityPoint, + message: Self::Message, + ) -> Result<(Self::NonIdentityPoint, Vec>), Error> { + let config = self.config().sinsemilla_config.clone(); + let chip = SinsemillaChip::::construct(config); + chip.hash_to_point_with_private_init(layouter, Q, message) + } +} diff --git a/halo2_gadgets/src/sinsemilla_opt/primitives.rs b/halo2_gadgets/src/sinsemilla_opt/primitives.rs new file mode 100644 index 0000000000..e4e5fbaaf8 --- /dev/null +++ b/halo2_gadgets/src/sinsemilla_opt/primitives.rs @@ -0,0 +1,76 @@ +//! Implementation of Sinsemilla outside the circuit. + +use group::Wnaf; +use halo2_proofs::arithmetic::CurveExt; +use pasta_curves::pallas; +use subtle::CtOption; + +use crate::sinsemilla::primitives::{CommitDomain, HashDomain}; + +impl CommitDomain { + /// Constructs a new `CommitDomain` from different values for `hash_domain` and `blind_domain` + pub fn new_with_personalization(hash_domain: &str, blind_domain: &str) -> Self { + let m_prefix = format!("{}-M", hash_domain); + let r_prefix = format!("{}-r", blind_domain); + let hasher_r = pallas::Point::hash_to_curve(&r_prefix); + CommitDomain { + M: HashDomain::new(&m_prefix), + R: hasher_r(&[]), + } + } + + /// $\mathsf{SinsemillaHashToPoint}$ from [§ 5.4.1.9][concretesinsemillahash]. + /// + /// [concretesinsemillahash]: https://zips.z.cash/protocol/nu5.pdf#concretesinsemillahash + pub fn hash_to_point(&self, msg: impl Iterator) -> CtOption { + self.M.hash_to_point(msg) + } + + /// Returns `SinsemillaCommit_r(personalization, msg) = hash_point + [r]R` + /// where `SinsemillaHash(personalization, msg) = hash_point` + /// and `R` is derived from the `personalization`. + #[allow(non_snake_case)] + pub fn commit_from_hash_point( + &self, + hash_point: CtOption, + r: &pallas::Scalar, + ) -> CtOption { + // We use complete addition for the blinding factor. + hash_point.map(|p| p + Wnaf::new().scalar(r).base(self.R)) + } +} + +#[cfg(test)] +mod tests { + use pasta_curves::{pallas}; + + + + #[test] + fn commit_in_several_steps() { + use rand::{rngs::OsRng, Rng}; + + use ff::Field; + + use crate::sinsemilla::primitives::CommitDomain; + + let domain = CommitDomain::new("z.cash:ZSA-NoteCommit"); + + let mut os_rng = OsRng::default(); + let msg: Vec = (0..36).map(|_| os_rng.gen::()).collect(); + + let rcm = pallas::Scalar::random(&mut os_rng); + + // Evaluate the commitment with commit function + let commit1 = domain.commit(msg.clone().into_iter(), &rcm); + + // Evaluate the commitment with the following steps + // 1. hash msg + // 2. evaluate the commitment from the hash point + let hash_point = domain.M.hash_to_point(msg.into_iter()); + let commit2 = domain.commit_from_hash_point(hash_point, &rcm); + + // Test equality + assert_eq!(commit1.unwrap(), commit2.unwrap()); + } +} diff --git a/halo2_gadgets/src/utilities/cond_swap.rs b/halo2_gadgets/src/utilities/cond_swap.rs index 46c0a5ded2..7712c89b2a 100644 --- a/halo2_gadgets/src/utilities/cond_swap.rs +++ b/halo2_gadgets/src/utilities/cond_swap.rs @@ -2,14 +2,12 @@ use super::{bool_check, ternary, UtilitiesInstructions}; -use crate::ecc::chip::{EccPoint, NonIdentityEccPoint}; use group::ff::{Field, PrimeField}; use halo2_proofs::{ circuit::{AssignedCell, Chip, Layouter, Value}, - plonk::{self, Advice, Column, ConstraintSystem, Constraints, Error, Selector}, + plonk::{Advice, Column, ConstraintSystem, Constraints, Error, Selector}, poly::Rotation, }; -use pasta_curves::pallas; use std::marker::PhantomData; /// Instructions for a conditional swap gadget. @@ -26,22 +24,12 @@ pub trait CondSwapInstructions: UtilitiesInstructions { pair: (Self::Var, Value), swap: Value, ) -> Result<(Self::Var, Self::Var), Error>; - - /// Given an input `(choice, left, right)` where `choice` is a boolean flag, - /// returns `left` if `choice` is not set and `right` if `choice` is set. - fn mux( - &self, - layouter: &mut impl Layouter, - choice: Self::Var, - left: Self::Var, - right: Self::Var, - ) -> Result; } /// A chip implementing a conditional swap. #[derive(Clone, Debug)] pub struct CondSwapChip { - config: CondSwapConfig, + pub(crate) config: CondSwapConfig, _marker: PhantomData, } @@ -61,12 +49,12 @@ impl Chip for CondSwapChip { /// Configuration for the [`CondSwapChip`]. #[derive(Clone, Debug, PartialEq, Eq)] pub struct CondSwapConfig { - q_swap: Selector, - a: Column, - b: Column, - a_swapped: Column, - b_swapped: Column, - swap: Column, + pub(crate) q_swap: Selector, + pub(crate) a: Column, + pub(crate) b: Column, + pub(crate) a_swapped: Column, + pub(crate) b_swapped: Column, + pub(crate) swap: Column, } #[cfg(test)] @@ -133,97 +121,6 @@ impl CondSwapInstructions for CondSwapChip { }, ) } - - fn mux( - &self, - layouter: &mut impl Layouter, - choice: Self::Var, - left: Self::Var, - right: Self::Var, - ) -> Result { - let config = self.config(); - - layouter.assign_region( - || "mux", - |mut region| { - // Enable `q_swap` selector - config.q_swap.enable(&mut region, 0)?; - - // Copy in `a` value - let left = left.copy_advice(|| "copy left", &mut region, config.a, 0)?; - - // Copy in `b` value - let right = right.copy_advice(|| "copy right", &mut region, config.b, 0)?; - - // Copy `choice` value - let choice = choice.copy_advice(|| "copy choice", &mut region, config.swap, 0)?; - - let a_swapped = left - .value() - .zip(right.value()) - .zip(choice.value()) - .map(|((left, right), choice)| { - if *choice == F::from(0_u64) { - left - } else { - right - } - }) - .cloned(); - let b_swapped = left - .value() - .zip(right.value()) - .zip(choice.value()) - .map(|((left, right), choice)| { - if *choice == F::from(0_u64) { - right - } else { - left - } - }) - .cloned(); - - region.assign_advice(|| "out b_swap", self.config.b_swapped, 0, || b_swapped)?; - region.assign_advice(|| "out a_swap", self.config.a_swapped, 0, || a_swapped) - }, - ) - } -} - -impl CondSwapChip { - /// Given an input `(choice, left, right)` where `choice` is a boolean flag and `left` and `right` are `EccPoint`, - /// returns `left` if `choice` is not set and `right` if `choice` is set. - pub fn mux_on_points( - &self, - mut layouter: impl Layouter, - choice: &AssignedCell, - left: &EccPoint, - right: &EccPoint, - ) -> Result { - let x_cell = self.mux(&mut layouter, choice.clone(), left.x(), right.x())?; - let y_cell = self.mux(&mut layouter, choice.clone(), left.y(), right.y())?; - Ok(EccPoint::from_coordinates_unchecked( - x_cell.into(), - y_cell.into(), - )) - } - - /// Given an input `(choice, left, right)` where `choice` is a boolean flag and `left` and `right` are - /// `NonIdentityEccPoint`, returns `left` if `choice` is not set and `right` if `choice` is set. - pub fn mux_on_non_identity_points( - &self, - mut layouter: impl Layouter, - choice: &AssignedCell, - left: &NonIdentityEccPoint, - right: &NonIdentityEccPoint, - ) -> Result { - let x_cell = self.mux(&mut layouter, choice.clone(), left.x(), right.x())?; - let y_cell = self.mux(&mut layouter, choice.clone(), left.y(), right.y())?; - Ok(NonIdentityEccPoint::from_coordinates_unchecked( - x_cell.into(), - y_cell.into(), - )) - } } impl CondSwapChip { @@ -394,231 +291,4 @@ mod tests { assert_eq!(prover.verify(), Ok(())); } } - - #[test] - fn test_mux() { - use crate::{ - ecc::{ - chip::{EccChip, EccConfig}, - tests::TestFixedBases, - NonIdentityPoint, Point, - }, - utilities::lookup_range_check::LookupRangeCheckConfig, - }; - - use group::{cofactor::CofactorCurveAffine, Curve, Group}; - use halo2_proofs::{ - circuit::{Layouter, SimpleFloorPlanner, Value}, - dev::MockProver, - plonk::{Advice, Circuit, Column, ConstraintSystem, Error, Instance}, - }; - use pasta_curves::arithmetic::CurveAffine; - use pasta_curves::{pallas, EpAffine}; - - use rand::rngs::OsRng; - - #[derive(Clone, Debug)] - pub struct MyConfig { - primary: Column, - advice: Column, - cond_swap_config: CondSwapConfig, - ecc_config: EccConfig, - } - - #[derive(Default)] - struct MyCircuit { - left_point: Value, - right_point: Value, - choice: Value, - } - - impl Circuit for MyCircuit { - type Config = MyConfig; - type FloorPlanner = SimpleFloorPlanner; - - fn without_witnesses(&self) -> Self { - Self::default() - } - - fn configure(meta: &mut ConstraintSystem) -> Self::Config { - let advices = [ - meta.advice_column(), - meta.advice_column(), - meta.advice_column(), - meta.advice_column(), - meta.advice_column(), - meta.advice_column(), - meta.advice_column(), - meta.advice_column(), - meta.advice_column(), - meta.advice_column(), - ]; - - for advice in advices.iter() { - meta.enable_equality(*advice); - } - - // Instance column used for public inputs - let primary = meta.instance_column(); - meta.enable_equality(primary); - - let cond_swap_config = - CondSwapChip::configure(meta, advices[0..5].try_into().unwrap()); - - let table_idx = meta.lookup_table_column(); - let table_range_check_tag = meta.lookup_table_column(); - - let lagrange_coeffs = [ - meta.fixed_column(), - meta.fixed_column(), - meta.fixed_column(), - meta.fixed_column(), - meta.fixed_column(), - meta.fixed_column(), - meta.fixed_column(), - meta.fixed_column(), - ]; - meta.enable_constant(lagrange_coeffs[0]); - - let range_check = LookupRangeCheckConfig::configure( - meta, - advices[9], - table_idx, - Some(table_range_check_tag), - ); - - let ecc_config = EccChip::::configure( - meta, - advices, - lagrange_coeffs, - range_check, - ); - - MyConfig { - primary, - advice: advices[0], - cond_swap_config, - ecc_config, - } - } - - fn synthesize( - &self, - config: Self::Config, - mut layouter: impl Layouter, - ) -> Result<(), Error> { - // Construct a CondSwap chip - let cond_swap_chip = CondSwapChip::construct(config.cond_swap_config); - - // Construct an ECC chip - let ecc_chip = EccChip::construct(config.ecc_config); - - // Assign choice - let choice = layouter.assign_region( - || "load private", - |mut region| { - region.assign_advice(|| "load private", config.advice, 0, || self.choice) - }, - )?; - - // Test mux on non identity points - // Assign left point - let left_non_identity_point = NonIdentityPoint::new( - ecc_chip.clone(), - layouter.namespace(|| "left point"), - self.left_point.map(|left_point| left_point), - )?; - - // Assign right point - let right_non_identity_point = NonIdentityPoint::new( - ecc_chip.clone(), - layouter.namespace(|| "right point"), - self.right_point.map(|right_point| right_point), - )?; - - // Apply mux - let result_non_identity_point = cond_swap_chip.mux_on_non_identity_points( - layouter.namespace(|| "MUX"), - &choice, - left_non_identity_point.inner(), - right_non_identity_point.inner(), - )?; - - // Check equality with instance - layouter.constrain_instance( - result_non_identity_point.x().cell(), - config.primary, - 0, - )?; - layouter.constrain_instance( - result_non_identity_point.y().cell(), - config.primary, - 1, - )?; - - // Test mux on points - // Assign left point - let left_point = Point::new( - ecc_chip.clone(), - layouter.namespace(|| "left point"), - self.left_point.map(|left_point| left_point), - )?; - - // Assign right point - let right_point = Point::new( - ecc_chip, - layouter.namespace(|| "right point"), - self.right_point.map(|right_point| right_point), - )?; - - // Apply mux - let result = cond_swap_chip.mux_on_points( - layouter.namespace(|| "MUX"), - &choice, - left_point.inner(), - right_point.inner(), - )?; - - // Check equality with instance - layouter.constrain_instance(result.x().cell(), config.primary, 0)?; - layouter.constrain_instance(result.y().cell(), config.primary, 1) - } - } - - // Test different circuits - let mut circuits = vec![]; - let mut instances = vec![]; - for choice in [false, true] { - let choice_value = if choice { - pallas::Base::one() - } else { - pallas::Base::zero() - }; - let left_point = pallas::Point::random(OsRng).to_affine(); - let right_point = pallas::Point::random(OsRng).to_affine(); - circuits.push(MyCircuit { - left_point: Value::known(left_point), - right_point: Value::known(right_point), - choice: Value::known(choice_value), - }); - let expected_output = if choice { right_point } else { left_point }; - let (expected_x, expected_y) = if bool::from(expected_output.is_identity()) { - (pallas::Base::zero(), pallas::Base::zero()) - } else { - let coords = expected_output.coordinates().unwrap(); - (*coords.x(), *coords.y()) - }; - instances.push([[expected_x, expected_y]]); - } - - for (circuit, instance) in circuits.iter().zip(instances.iter()) { - let prover = MockProver::::run( - 5, - circuit, - instance.iter().map(|p| p.to_vec()).collect(), - ) - .unwrap(); - assert_eq!(prover.verify(), Ok(())); - } - } } diff --git a/halo2_gadgets/src/utilities_opt.rs b/halo2_gadgets/src/utilities_opt.rs new file mode 100644 index 0000000000..427234e8d9 --- /dev/null +++ b/halo2_gadgets/src/utilities_opt.rs @@ -0,0 +1,3 @@ +//! Utility gadgets. + +pub mod cond_swap; diff --git a/halo2_gadgets/src/utilities_opt/cond_swap.rs b/halo2_gadgets/src/utilities_opt/cond_swap.rs new file mode 100644 index 0000000000..afd82cae3b --- /dev/null +++ b/halo2_gadgets/src/utilities_opt/cond_swap.rs @@ -0,0 +1,345 @@ +//! Gadget and chip for a conditional swap utility. + +use group::ff::{Field, PrimeField}; +use pasta_curves::pallas; + +use halo2_proofs::{ + circuit::{AssignedCell, Chip, Layouter}, + plonk::{self, Error}, +}; + +use crate::ecc::chip::{EccPoint, NonIdentityEccPoint}; + +use crate::utilities::cond_swap::{CondSwapChip, CondSwapInstructions}; + +/// Instructions for a conditional swap gadget. +pub trait CondSwapInstructionsOptimized: CondSwapInstructions { + /// Given an input `(choice, left, right)` where `choice` is a boolean flag, + /// returns `left` if `choice` is not set and `right` if `choice` is set. + fn mux( + &self, + layouter: &mut impl Layouter, + choice: Self::Var, + left: Self::Var, + right: Self::Var, + ) -> Result; +} + +impl CondSwapInstructionsOptimized for CondSwapChip { + fn mux( + &self, + layouter: &mut impl Layouter, + choice: Self::Var, + left: Self::Var, + right: Self::Var, + ) -> Result { + let config = self.config(); + + layouter.assign_region( + || "mux", + |mut region| { + // Enable `q_swap` selector + config.q_swap.enable(&mut region, 0)?; + + // Copy in `a` value + let left = left.copy_advice(|| "copy left", &mut region, config.a, 0)?; + + // Copy in `b` value + let right = right.copy_advice(|| "copy right", &mut region, config.b, 0)?; + + // Copy `choice` value + let choice = choice.copy_advice(|| "copy choice", &mut region, config.swap, 0)?; + + let a_swapped = left + .value() + .zip(right.value()) + .zip(choice.value()) + .map(|((left, right), choice)| { + if *choice == F::from(0_u64) { + left + } else { + right + } + }) + .cloned(); + let b_swapped = left + .value() + .zip(right.value()) + .zip(choice.value()) + .map(|((left, right), choice)| { + if *choice == F::from(0_u64) { + right + } else { + left + } + }) + .cloned(); + + region.assign_advice(|| "out b_swap", self.config.b_swapped, 0, || b_swapped)?; + region.assign_advice(|| "out a_swap", self.config.a_swapped, 0, || a_swapped) + }, + ) + } +} + +impl CondSwapChip { + /// Given an input `(choice, left, right)` where `choice` is a boolean flag and `left` and `right` are `EccPoint`, + /// returns `left` if `choice` is not set and `right` if `choice` is set. + pub fn mux_on_points( + &self, + mut layouter: impl Layouter, + choice: &AssignedCell, + left: &EccPoint, + right: &EccPoint, + ) -> Result { + let x_cell = self.mux(&mut layouter, choice.clone(), left.x(), right.x())?; + let y_cell = self.mux(&mut layouter, choice.clone(), left.y(), right.y())?; + Ok(EccPoint::from_coordinates_unchecked( + x_cell.into(), + y_cell.into(), + )) + } + + /// Given an input `(choice, left, right)` where `choice` is a boolean flag and `left` and `right` are + /// `NonIdentityEccPoint`, returns `left` if `choice` is not set and `right` if `choice` is set. + pub fn mux_on_non_identity_points( + &self, + mut layouter: impl Layouter, + choice: &AssignedCell, + left: &NonIdentityEccPoint, + right: &NonIdentityEccPoint, + ) -> Result { + let x_cell = self.mux(&mut layouter, choice.clone(), left.x(), right.x())?; + let y_cell = self.mux(&mut layouter, choice.clone(), left.y(), right.y())?; + Ok(NonIdentityEccPoint::from_coordinates_unchecked( + x_cell.into(), + y_cell.into(), + )) + } +} + +#[cfg(test)] +mod tests { + use crate::utilities::cond_swap::{CondSwapChip, CondSwapConfig}; + + #[test] + fn test_mux() { + use crate::{ + ecc::{ + chip::{EccChip, EccConfig}, + tests::TestFixedBases, + NonIdentityPoint, Point, + }, + utilities::lookup_range_check::LookupRangeCheckConfig, + }; + + use group::{cofactor::CofactorCurveAffine, Curve, Group}; + use halo2_proofs::{ + circuit::{Layouter, SimpleFloorPlanner, Value}, + dev::MockProver, + plonk::{Advice, Circuit, Column, ConstraintSystem, Error, Instance}, + }; + use pasta_curves::arithmetic::CurveAffine; + use pasta_curves::{pallas, EpAffine}; + + use rand::rngs::OsRng; + + #[derive(Clone, Debug)] + pub struct MyConfig { + primary: Column, + advice: Column, + cond_swap_config: CondSwapConfig, + ecc_config: EccConfig, + } + + #[derive(Default)] + struct MyCircuit { + left_point: Value, + right_point: Value, + choice: Value, + } + + impl Circuit for MyCircuit { + type Config = MyConfig; + type FloorPlanner = SimpleFloorPlanner; + + fn without_witnesses(&self) -> Self { + Self::default() + } + + fn configure(meta: &mut ConstraintSystem) -> Self::Config { + let advices = [ + meta.advice_column(), + meta.advice_column(), + meta.advice_column(), + meta.advice_column(), + meta.advice_column(), + meta.advice_column(), + meta.advice_column(), + meta.advice_column(), + meta.advice_column(), + meta.advice_column(), + ]; + + for advice in advices.iter() { + meta.enable_equality(*advice); + } + + // Instance column used for public inputs + let primary = meta.instance_column(); + meta.enable_equality(primary); + + let cond_swap_config = + CondSwapChip::configure(meta, advices[0..5].try_into().unwrap()); + + let table_idx = meta.lookup_table_column(); + + let lagrange_coeffs = [ + meta.fixed_column(), + meta.fixed_column(), + meta.fixed_column(), + meta.fixed_column(), + meta.fixed_column(), + meta.fixed_column(), + meta.fixed_column(), + meta.fixed_column(), + ]; + meta.enable_constant(lagrange_coeffs[0]); + + let range_check = LookupRangeCheckConfig::configure(meta, advices[9], table_idx); + + let ecc_config = EccChip::::configure( + meta, + advices, + lagrange_coeffs, + range_check, + ); + + MyConfig { + primary, + advice: advices[0], + cond_swap_config, + ecc_config, + } + } + + fn synthesize( + &self, + config: Self::Config, + mut layouter: impl Layouter, + ) -> Result<(), Error> { + // Construct a CondSwap chip + let cond_swap_chip = CondSwapChip::construct(config.cond_swap_config); + + // Construct an ECC chip + let ecc_chip = EccChip::construct(config.ecc_config); + + // Assign choice + let choice = layouter.assign_region( + || "load private", + |mut region| { + region.assign_advice(|| "load private", config.advice, 0, || self.choice) + }, + )?; + + // Test mux on non identity points + // Assign left point + let left_non_identity_point = NonIdentityPoint::new( + ecc_chip.clone(), + layouter.namespace(|| "left point"), + self.left_point.map(|left_point| left_point), + )?; + + // Assign right point + let right_non_identity_point = NonIdentityPoint::new( + ecc_chip.clone(), + layouter.namespace(|| "right point"), + self.right_point.map(|right_point| right_point), + )?; + + // Apply mux + let result_non_identity_point = cond_swap_chip.mux_on_non_identity_points( + layouter.namespace(|| "MUX"), + &choice, + left_non_identity_point.inner(), + right_non_identity_point.inner(), + )?; + + // Check equality with instance + layouter.constrain_instance( + result_non_identity_point.x().cell(), + config.primary, + 0, + )?; + layouter.constrain_instance( + result_non_identity_point.y().cell(), + config.primary, + 1, + )?; + + // Test mux on points + // Assign left point + let left_point = Point::new( + ecc_chip.clone(), + layouter.namespace(|| "left point"), + self.left_point.map(|left_point| left_point), + )?; + + // Assign right point + let right_point = Point::new( + ecc_chip, + layouter.namespace(|| "right point"), + self.right_point.map(|right_point| right_point), + )?; + + // Apply mux + let result = cond_swap_chip.mux_on_points( + layouter.namespace(|| "MUX"), + &choice, + left_point.inner(), + right_point.inner(), + )?; + + // Check equality with instance + layouter.constrain_instance(result.x().cell(), config.primary, 0)?; + layouter.constrain_instance(result.y().cell(), config.primary, 1) + } + } + + // Test different circuits + let mut circuits = vec![]; + let mut instances = vec![]; + for choice in [false, true] { + let choice_value = if choice { + pallas::Base::one() + } else { + pallas::Base::zero() + }; + let left_point = pallas::Point::random(OsRng).to_affine(); + let right_point = pallas::Point::random(OsRng).to_affine(); + circuits.push(MyCircuit { + left_point: Value::known(left_point), + right_point: Value::known(right_point), + choice: Value::known(choice_value), + }); + let expected_output = if choice { right_point } else { left_point }; + let (expected_x, expected_y) = if bool::from(expected_output.is_identity()) { + (pallas::Base::zero(), pallas::Base::zero()) + } else { + let coords = expected_output.coordinates().unwrap(); + (*coords.x(), *coords.y()) + }; + instances.push([[expected_x, expected_y]]); + } + + for (circuit, instance) in circuits.iter().zip(instances.iter()) { + let prover = MockProver::::run( + 5, + circuit, + instance.iter().map(|p| p.to_vec()).collect(), + ) + .unwrap(); + assert_eq!(prover.verify(), Ok(())); + } + } +}