From 35c815d36ed271d0a6542a664465590f2362fa8f Mon Sep 17 00:00:00 2001 From: Constance Date: Tue, 21 Mar 2023 17:04:19 +0100 Subject: [PATCH 01/15] Add variable-base sign-scalar multiplication --- halo2_gadgets/src/ecc.rs | 34 ++- halo2_gadgets/src/ecc/chip.rs | 18 ++ halo2_gadgets/src/ecc/chip/mul_fixed/short.rs | 279 +++++++++++++++++- 3 files changed, 328 insertions(+), 3 deletions(-) diff --git a/halo2_gadgets/src/ecc.rs b/halo2_gadgets/src/ecc.rs index 38fab0a49a..4bf58d3ec6 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::{Chip, Layouter, Value}, + circuit::{AssignedCell, Chip, Layouter, Value}, plonk::Error, }; @@ -111,6 +111,15 @@ 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, @@ -432,6 +441,21 @@ 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. @@ -865,6 +889,14 @@ pub(crate) mod tests { )?; } + // Test variable-base sign-scalar multiplication + { + super::chip::mul_fixed::short::tests::test_mul_sign( + chip.clone(), + layouter.namespace(|| "variable-base sign-scalar mul"), + )?; + } + // Test full-width fixed-base scalar multiplication { super::chip::mul_fixed::full_width::tests::test_mul_fixed( diff --git a/halo2_gadgets/src/ecc/chip.rs b/halo2_gadgets/src/ecc/chip.rs index 4d12057afb..3766b454d9 100644 --- a/halo2_gadgets/src/ecc/chip.rs +++ b/halo2_gadgets/src/ecc/chip.rs @@ -532,6 +532,24 @@ 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/short.rs b/halo2_gadgets/src/ecc/chip/mul_fixed/short.rs index bfdc735f70..94d61de54a 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::{Layouter, Region}, + circuit::{AssignedCell, Layouter, Region}, plonk::{ConstraintSystem, Constraints, Error, Expression, Selector}, poly::Rotation, }; @@ -241,11 +241,73 @@ 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}; + use group::{ff::PrimeField, Curve, Group}; use halo2_proofs::{ arithmetic::CurveAffine, circuit::{AssignedCell, Chip, Layouter, Value}, @@ -657,4 +719,217 @@ 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 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()), + ], + } + ]) + ); + } } From 731bc1021a000cc14b05d952571ded51bf6a15ea Mon Sep 17 00:00:00 2001 From: Constance Date: Wed, 19 Apr 2023 11:01:24 +0200 Subject: [PATCH 02/15] Add CommitDomain creation from two personalizations --- halo2_gadgets/src/sinsemilla/primitives.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/halo2_gadgets/src/sinsemilla/primitives.rs b/halo2_gadgets/src/sinsemilla/primitives.rs index 9bf6a72332..63aeec376c 100644 --- a/halo2_gadgets/src/sinsemilla/primitives.rs +++ b/halo2_gadgets/src/sinsemilla/primitives.rs @@ -200,6 +200,21 @@ impl CommitDomain { } } + /// Constructs a new `CommitDomain` from `hash_personalization` a personalization for hashing + /// and `blind_personalization` another personalization for blinding. + pub fn new_with_two_personalizations( + hash_personalization: &str, + blind_personalization: &str, + ) -> Self { + let m_prefix = format!("{}-M", hash_personalization); + let r_prefix = format!("{}-r", blind_personalization); + 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 From b1e397f5b1e2e7e43741e69ebafa3a1680a2ab84 Mon Sep 17 00:00:00 2001 From: Constance Date: Wed, 19 Apr 2023 09:26:02 +0200 Subject: [PATCH 03/15] Add commit_from_hash_point --- halo2_gadgets/src/sinsemilla/primitives.rs | 44 +++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/halo2_gadgets/src/sinsemilla/primitives.rs b/halo2_gadgets/src/sinsemilla/primitives.rs index 63aeec376c..281a76d504 100644 --- a/halo2_gadgets/src/sinsemilla/primitives.rs +++ b/halo2_gadgets/src/sinsemilla/primitives.rs @@ -184,7 +184,8 @@ impl HashDomain { #[derive(Debug)] #[allow(non_snake_case)] pub struct CommitDomain { - M: HashDomain, + /// A domain in which $\mathsf{SinsemillaHashToPoint}$ and $\mathsf{SinsemillaHash}$ can be used + pub M: HashDomain, R: pallas::Point, } @@ -229,6 +230,19 @@ impl CommitDomain { .map(|p| p + Wnaf::new().scalar(r).base(self.R)) } + /// 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 @@ -320,4 +334,32 @@ 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()); + } } From 8cfe0ae67df157868c1838321fb39d68786fbcf2 Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 20 Apr 2023 17:02:03 +0300 Subject: [PATCH 04/15] updated naming --- halo2_gadgets/src/sinsemilla/primitives.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/halo2_gadgets/src/sinsemilla/primitives.rs b/halo2_gadgets/src/sinsemilla/primitives.rs index 281a76d504..f90c63ac12 100644 --- a/halo2_gadgets/src/sinsemilla/primitives.rs +++ b/halo2_gadgets/src/sinsemilla/primitives.rs @@ -201,14 +201,10 @@ impl CommitDomain { } } - /// Constructs a new `CommitDomain` from `hash_personalization` a personalization for hashing - /// and `blind_personalization` another personalization for blinding. - pub fn new_with_two_personalizations( - hash_personalization: &str, - blind_personalization: &str, - ) -> Self { - let m_prefix = format!("{}-M", hash_personalization); - let r_prefix = format!("{}-r", blind_personalization); + /// 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), @@ -230,7 +226,7 @@ impl CommitDomain { .map(|p| p + Wnaf::new().scalar(r).base(self.R)) } - /// Returns `SinsemillaCommit_r(personalization, msg) = hash_point + \[r\]R` + /// 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)] From 54697b22ed293fd45fe3021552b29204bd91aaeb Mon Sep 17 00:00:00 2001 From: Constance Date: Mon, 17 Apr 2023 16:51:53 +0200 Subject: [PATCH 05/15] Add hash and blinding_factor functions --- halo2_gadgets/src/sinsemilla.rs | 39 +++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/halo2_gadgets/src/sinsemilla.rs b/halo2_gadgets/src/sinsemilla.rs index 3f06315a77..ca4c14fb1d 100644 --- a/halo2_gadgets/src/sinsemilla.rs +++ b/halo2_gadgets/src/sinsemilla.rs @@ -412,6 +412,41 @@ where } } + #[allow(clippy::type_complexity)] + /// $\mathsf{SinsemillaCommit}$ from [§ 5.4.8.4][concretesinsemillacommit]. + /// + /// [concretesinsemillacommit]: https://zips.z.cash/protocol/nu5.pdf#concretesinsemillacommit + 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(clippy::type_complexity)] + /// $\mathsf{SinsemillaCommit}$ from [§ 5.4.8.4][concretesinsemillacommit]. + /// + /// [concretesinsemillacommit]: https://zips.z.cash/protocol/nu5.pdf#concretesinsemillacommit + 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]. /// @@ -429,8 +464,8 @@ where Error, > { assert_eq!(self.M.sinsemilla_chip, message.chip); - let (blind, _) = self.R.mul(layouter.namespace(|| "[r] R"), r)?; - let (p, zs) = self.M.hash_to_point(layouter.namespace(|| "M"), message)?; + let blind = self.blinding_factor(layouter.namespace(|| "[r] R"), r)?; + let (p, zs) = self.hash(layouter.namespace(|| "M"), message)?; let commitment = p.add(layouter.namespace(|| "M + [r] R"), &blind)?; Ok((commitment, zs)) } From 4ce262d8e7d98a9af5f3edfa9fec489b38f35f27 Mon Sep 17 00:00:00 2001 From: Constance Date: Tue, 18 Apr 2023 10:19:25 +0200 Subject: [PATCH 06/15] Add some functionalities for MuxChip --- halo2_gadgets/src/ecc/chip.rs | 2 +- halo2_proofs/src/circuit.rs | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/halo2_gadgets/src/ecc/chip.rs b/halo2_gadgets/src/ecc/chip.rs index 3766b454d9..0b6fa0d4b4 100644 --- a/halo2_gadgets/src/ecc/chip.rs +++ b/halo2_gadgets/src/ecc/chip.rs @@ -48,7 +48,7 @@ impl EccPoint { /// Constructs a point from its coordinates, without checking they are on the curve. /// /// This is an internal API that we only use where we know we have a valid curve point. - pub(crate) fn from_coordinates_unchecked( + pub fn from_coordinates_unchecked( x: AssignedCell, pallas::Base>, y: AssignedCell, pallas::Base>, ) -> Self { diff --git a/halo2_proofs/src/circuit.rs b/halo2_proofs/src/circuit.rs index 0822d8d8aa..1083339fed 100644 --- a/halo2_proofs/src/circuit.rs +++ b/halo2_proofs/src/circuit.rs @@ -139,6 +139,16 @@ impl AssignedCell, F> { } } +impl From> for AssignedCell, F> { + fn from(ac: AssignedCell) -> Self { + AssignedCell { + value: ac.value.map(|a| a.into()), + cell: ac.cell, + _marker: Default::default(), + } + } +} + impl AssignedCell where for<'v> Assigned: From<&'v V>, From 475f54daa4a15adb3920f5231d3d4a00c0ca7b45 Mon Sep 17 00:00:00 2001 From: Constance Beguier Date: Fri, 23 Jun 2023 10:31:10 +0200 Subject: [PATCH 07/15] Add Point::new_from_constant method (#17) It is now possible to create a Point from a constant. This functionality is required to evaluate the old nullifier. - for non split_notes, nf_old = Extract_P([PRF^{nfOrchard}_{nk}(rho_old) + psi_nf) mod q_P] NullifierK + cm_old) - for split notes, nf_old = Extract_P([PRF^{nfOrchard}_{nk}(rho_old) + psi_nf) mod q_P] NullifierK + cm_old + NullifierL) --- halo2_gadgets/src/ecc.rs | 19 +++++++++++ halo2_gadgets/src/ecc/chip.rs | 12 +++++++ halo2_gadgets/src/ecc/chip/witness_point.rs | 37 +++++++++++++++++++++ 3 files changed, 68 insertions(+) diff --git a/halo2_gadgets/src/ecc.rs b/halo2_gadgets/src/ecc.rs index 4bf58d3ec6..2e6e509190 100644 --- a/halo2_gadgets/src/ecc.rs +++ b/halo2_gadgets/src/ecc.rs @@ -60,6 +60,15 @@ 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( @@ -399,6 +408,16 @@ 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, diff --git a/halo2_gadgets/src/ecc/chip.rs b/halo2_gadgets/src/ecc/chip.rs index 0b6fa0d4b4..119b1670f6 100644 --- a/halo2_gadgets/src/ecc/chip.rs +++ b/halo2_gadgets/src/ecc/chip.rs @@ -453,6 +453,18 @@ 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, diff --git a/halo2_gadgets/src/ecc/chip/witness_point.rs b/halo2_gadgets/src/ecc/chip/witness_point.rs index 7cba8d6f87..28e8aaf4a9 100644 --- a/halo2_gadgets/src/ecc/chip/witness_point.rs +++ b/halo2_gadgets/src/ecc/chip/witness_point.rs @@ -102,6 +102,21 @@ impl Config { Ok((x_var, y_var)) } + fn assign_xy_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, @@ -126,6 +141,28 @@ 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_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, From 4c3c00bced34bb9c86ee978c9195dbe1b71dcc05 Mon Sep 17 00:00:00 2001 From: Constance Beguier Date: Mon, 16 Oct 2023 09:27:33 +0200 Subject: [PATCH 08/15] Optimized short range check on 4 and 5 bits (#21) Short range checks on 4 and 5 bits are now performed with only one lookup (instead of 2). To do that, we added a column `table_short_range_tag` in the lookup table. This new column `table_short_range_tag` contains the value - 4 for rows used in short range check on 4 bits - 5 for rows used in short range check on 5 bits - 0 for rows used in short range check on 10 bits Disable tests on i686 and code coverage in CI --- .github/workflows/ci.yml | 114 +++++------ halo2_gadgets/src/ecc.rs | 8 +- halo2_gadgets/src/ecc/chip/mul_fixed/short.rs | 18 +- halo2_gadgets/src/sinsemilla.rs | 9 +- halo2_gadgets/src/sinsemilla/chip.rs | 3 +- .../src/sinsemilla/chip/generator_table.rs | 63 +++++- halo2_gadgets/src/sinsemilla/merkle.rs | 4 +- .../src/utilities/lookup_range_check.rs | 186 +++++++++++++++--- 8 files changed, 308 insertions(+), 97 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d2ecd3ba57..db9abdc62e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,36 +33,36 @@ jobs: ${{ steps.prepare.outputs.feature-flags }} ${{ matrix.extra_flags }} - test-32-bit: - name: Test on i686-unknown-linux-gnu${{ matrix.name_suffix }} - runs-on: ubuntu-latest - strategy: - matrix: - stage: [stable, beta, nightly] - include: - - stage: beta - name_suffix: " with beta features" - - stage: nightly - name_suffix: " with nightly features" - - steps: - - uses: actions/checkout@v3 - - id: prepare - uses: ./.github/actions/prepare - with: - beta-features: ${{ matrix.stage == 'beta' }} - nightly-features: ${{ matrix.stage == 'nightly' }} - - name: Install cross-platform support dependencies - run: sudo apt install gcc-multilib - - run: rustup target add i686-unknown-linux-gnu - - name: Run tests - run: > - cargo test - --verbose - --release - --workspace - --target i686-unknown-linux-gnu - ${{ steps.prepare.outputs.feature-flags }} +# test-32-bit: +# name: Test on i686-unknown-linux-gnu${{ matrix.name_suffix }} +# runs-on: ubuntu-latest +# strategy: +# matrix: +# stage: [stable, beta, nightly] +# include: +# - stage: beta +# name_suffix: " with beta features" +# - stage: nightly +# name_suffix: " with nightly features" +# +# steps: +# - uses: actions/checkout@v3 +# - id: prepare +# uses: ./.github/actions/prepare +# with: +# beta-features: ${{ matrix.stage == 'beta' }} +# nightly-features: ${{ matrix.stage == 'nightly' }} +# - name: Install cross-platform support dependencies +# run: sudo apt install gcc-multilib +# - run: rustup target add i686-unknown-linux-gnu +# - name: Run tests +# run: > +# cargo test +# --verbose +# --release +# --workspace +# --target i686-unknown-linux-gnu +# ${{ steps.prepare.outputs.feature-flags }} build: name: Build target ${{ matrix.target }} @@ -125,33 +125,33 @@ jobs: - name: Test halo2 book run: mdbook test -L target/debug/deps book/ - codecov: - name: Code coverage - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - # Use stable for this to ensure that cargo-tarpaulin can be built. - - id: prepare - uses: ./.github/actions/prepare - with: - toolchain: stable - nightly-features: true - - name: Install cargo-tarpaulin - uses: actions-rs/cargo@v1 - with: - command: install - args: cargo-tarpaulin - - name: Generate coverage report - uses: actions-rs/cargo@v1 - with: - command: tarpaulin - args: > - ${{ steps.prepare.outputs.feature-flags }} - --timeout 600 - --out Xml - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3.1.4 +# codecov: +# name: Code coverage +# runs-on: ubuntu-latest +# +# steps: +# - uses: actions/checkout@v3 +# # Use stable for this to ensure that cargo-tarpaulin can be built. +# - id: prepare +# uses: ./.github/actions/prepare +# with: +# toolchain: stable +# nightly-features: true +# - name: Install cargo-tarpaulin +# uses: actions-rs/cargo@v1 +# with: +# command: install +# args: cargo-tarpaulin +# - name: Generate coverage report +# uses: actions-rs/cargo@v1 +# with: +# command: tarpaulin +# args: > +# ${{ steps.prepare.outputs.feature-flags }} +# --timeout 600 +# --out Xml +# - name: Upload coverage to Codecov +# uses: codecov/codecov-action@v3.1.4 doc-links: name: Intra-doc links diff --git a/halo2_gadgets/src/ecc.rs b/halo2_gadgets/src/ecc.rs index 2e6e509190..4861a20e52 100644 --- a/halo2_gadgets/src/ecc.rs +++ b/halo2_gadgets/src/ecc.rs @@ -793,6 +793,7 @@ 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(), @@ -807,7 +808,12 @@ pub(crate) mod tests { let constants = meta.fixed_column(); meta.enable_constant(constants); - let range_check = LookupRangeCheckConfig::configure(meta, advices[9], lookup_table); + let range_check = LookupRangeCheckConfig::configure( + meta, + advices[9], + lookup_table, + table_range_check_tag, + ); EccChip::::configure(meta, advices, lagrange_coeffs, range_check) } diff --git a/halo2_gadgets/src/ecc/chip/mul_fixed/short.rs b/halo2_gadgets/src/ecc/chip/mul_fixed/short.rs index 94d61de54a..a10c54c1ed 100644 --- a/halo2_gadgets/src/ecc/chip/mul_fixed/short.rs +++ b/halo2_gadgets/src/ecc/chip/mul_fixed/short.rs @@ -508,6 +508,7 @@ 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(), @@ -523,7 +524,12 @@ pub mod tests { let constants = meta.fixed_column(); meta.enable_constant(constants); - let range_check = LookupRangeCheckConfig::configure(meta, advices[9], lookup_table); + let range_check = LookupRangeCheckConfig::configure( + meta, + advices[9], + lookup_table, + table_range_check_tag, + ); EccChip::::configure(meta, advices, lagrange_coeffs, range_check) } @@ -644,7 +650,7 @@ pub mod tests { )], }, VerifyFailure::Permutation { - column: (Any::Fixed, 9).into(), + column: (Any::Fixed, 10).into(), location: FailureLocation::OutsideRegion { row: 0 }, }, VerifyFailure::Permutation { @@ -827,6 +833,7 @@ 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(), @@ -842,7 +849,12 @@ pub mod tests { let constants = meta.fixed_column(); meta.enable_constant(constants); - let range_check = LookupRangeCheckConfig::configure(meta, advices[9], lookup_table); + let range_check = LookupRangeCheckConfig::configure( + meta, + advices[9], + lookup_table, + table_range_check_tag, + ); EccChip::::configure(meta, advices, lagrange_coeffs, range_check) } diff --git a/halo2_gadgets/src/sinsemilla.rs b/halo2_gadgets/src/sinsemilla.rs index ca4c14fb1d..a1dcb9d2a2 100644 --- a/halo2_gadgets/src/sinsemilla.rs +++ b/halo2_gadgets/src/sinsemilla.rs @@ -586,6 +586,7 @@ 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(), @@ -602,9 +603,15 @@ pub(crate) mod tests { table_idx, meta.lookup_table_column(), meta.lookup_table_column(), + table_range_check_tag, ); - let range_check = LookupRangeCheckConfig::configure(meta, advices[9], table_idx); + let range_check = LookupRangeCheckConfig::configure( + meta, + advices[9], + table_idx, + table_range_check_tag, + ); 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 ac4c34f781..ef76b19815 100644 --- a/halo2_gadgets/src/sinsemilla/chip.rs +++ b/halo2_gadgets/src/sinsemilla/chip.rs @@ -153,7 +153,7 @@ where advices: [Column; 5], witness_pieces: Column, fixed_y_q: Column, - lookup: (TableColumn, TableColumn, TableColumn), + lookup: (TableColumn, TableColumn, TableColumn, TableColumn), range_check: LookupRangeCheckConfig, ) -> >::Config { // Enable equality on all advice columns @@ -178,6 +178,7 @@ where table_idx: lookup.0, table_x: lookup.1, table_y: lookup.2, + table_range_check_tag: lookup.3, }, lookup_config: range_check, _marker: PhantomData, diff --git a/halo2_gadgets/src/sinsemilla/chip/generator_table.rs b/halo2_gadgets/src/sinsemilla/chip/generator_table.rs index fd0ff03f91..13773a4659 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, SINSEMILLA_S}; +use crate::sinsemilla::primitives::{self as sinsemilla, K, SINSEMILLA_S}; use pasta_curves::pallas; /// Table containing independent generators S[0..2^k] @@ -15,6 +15,7 @@ pub struct GeneratorTableConfig { pub table_idx: TableColumn, pub table_x: TableColumn, pub table_y: TableColumn, + pub table_range_check_tag: TableColumn, } impl GeneratorTableConfig { @@ -90,6 +91,66 @@ 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))?; + table.assign_cell( + || "table_range_check_tag", + self.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", + self.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", + self.table_range_check_tag, + new_index, + || Value::known(pallas::Base::from(5_u64)), + )?; + } } Ok(()) }, diff --git a/halo2_gadgets/src/sinsemilla/merkle.rs b/halo2_gadgets/src/sinsemilla/merkle.rs index 02a3bdaf7c..1eabfaa870 100644 --- a/halo2_gadgets/src/sinsemilla/merkle.rs +++ b/halo2_gadgets/src/sinsemilla/merkle.rs @@ -246,9 +246,11 @@ pub mod tests { meta.lookup_table_column(), meta.lookup_table_column(), meta.lookup_table_column(), + meta.lookup_table_column(), ); - let range_check = LookupRangeCheckConfig::configure(meta, advices[9], lookup.0); + let range_check = + LookupRangeCheckConfig::configure(meta, advices[9], lookup.0, lookup.3); let sinsemilla_config_1 = SinsemillaChip::configure( meta, diff --git a/halo2_gadgets/src/utilities/lookup_range_check.rs b/halo2_gadgets/src/utilities/lookup_range_check.rs index b26a89a884..d1173babbc 100644 --- a/halo2_gadgets/src/utilities/lookup_range_check.rs +++ b/halo2_gadgets/src/utilities/lookup_range_check.rs @@ -60,8 +60,11 @@ pub struct LookupRangeCheckConfig { q_lookup: Selector, q_running: Selector, q_bitshift: Selector, + q_range_check_4: Selector, + q_range_check_5: Selector, running_sum: Column, table_idx: TableColumn, + table_range_check_tag: TableColumn, _marker: PhantomData, } @@ -81,18 +84,24 @@ impl LookupRangeCheckConfig { meta: &mut ConstraintSystem, running_sum: Column, table_idx: TableColumn, + table_range_check_tag: TableColumn, ) -> Self { meta.enable_equality(running_sum); let q_lookup = meta.complex_selector(); let q_running = meta.complex_selector(); let q_bitshift = meta.selector(); + let q_range_check_4 = meta.complex_selector(); + let q_range_check_5 = meta.complex_selector(); let config = LookupRangeCheckConfig { q_lookup, q_running, q_bitshift, + q_range_check_4, + q_range_check_5, running_sum, table_idx, + table_range_check_tag, _marker: PhantomData, }; @@ -100,7 +109,10 @@ impl LookupRangeCheckConfig { meta.lookup(|meta| { let q_lookup = meta.query_selector(config.q_lookup); let q_running = meta.query_selector(config.q_running); + let q_range_check_4 = meta.query_selector(config.q_range_check_4); + let q_range_check_5 = meta.query_selector(config.q_range_check_5); let z_cur = meta.query_advice(config.running_sum, Rotation::cur()); + let one = Expression::Constant(F::ONE); // In the case of a running sum decomposition, we recover the word from // the difference of the running sums: @@ -117,17 +129,40 @@ impl LookupRangeCheckConfig { // In the short range check, the word is directly witnessed. let short_lookup = { - let short_word = z_cur; - let q_short = Expression::Constant(F::ONE) - q_running; + let short_word = z_cur.clone(); + let q_short = one.clone() - q_running; q_short * short_word }; + // q_range_check is equal to + // - 1 if q_range_check_4 = 1 or q_range_check_5 = 1 + // - 0 otherwise + let q_range_check = one.clone() + - (one.clone() - q_range_check_4.clone()) * (one.clone() - q_range_check_5.clone()); + + // num_bits is equal to + // - 5 if q_range_check_5 = 1 + // - 4 if q_range_check_4 = 1 and q_range_check_5 = 0 + // - 0 otherwise + let num_bits = q_range_check_5.clone() * Expression::Constant(F::from(5_u64)) + + (one.clone() - q_range_check_5) + * q_range_check_4 + * Expression::Constant(F::from(4_u64)); + // Combine the running sum and short lookups: - vec![( - q_lookup * (running_sum_lookup + short_lookup), - config.table_idx, - )] + vec![ + ( + q_lookup.clone() + * ((one - q_range_check.clone()) * (running_sum_lookup + short_lookup) + + q_range_check.clone() * z_cur), + config.table_idx, + ), + ( + q_lookup * q_range_check * num_bits, + config.table_range_check_tag, + ), + ] }); // For short lookups, check that the word has been shifted by the correct number of bits. @@ -152,9 +187,9 @@ impl LookupRangeCheckConfig { } #[cfg(test)] - // Loads the values [0..2^K) into `table_idx`. This is only used in testing - // for now, since the Sinsemilla chip provides a pre-loaded table in the - // Orchard context. + // Fill `table_idx` and `table_range_check_tag`. + // This is only used in testing for now, since the Sinsemilla chip provides a pre-loaded table + // in the Orchard context. pub fn load(&self, layouter: &mut impl Layouter) -> Result<(), Error> { layouter.assign_table( || "table_idx", @@ -167,6 +202,42 @@ impl LookupRangeCheckConfig { index, || Value::known(F::from(index as u64)), )?; + table.assign_cell( + || "table_range_check_tag", + self.table_range_check_tag, + index, + || Value::known(F::ZERO), + )?; + } + for index in 0..(1 << 4) { + let new_index = index + (1 << K); + table.assign_cell( + || "table_idx", + self.table_idx, + new_index, + || Value::known(F::from(index as u64)), + )?; + table.assign_cell( + || "table_range_check_tag", + self.table_range_check_tag, + new_index, + || Value::known(F::from(4_u64)), + )?; + } + for index in 0..(1 << 5) { + let new_index = index + (1 << K) + (1 << 4); + table.assign_cell( + || "table_idx", + self.table_idx, + new_index, + || Value::known(F::from(index as u64)), + )?; + table.assign_cell( + || "table_range_check_tag", + self.table_range_check_tag, + new_index, + || Value::known(F::from(5_u64)), + )?; } Ok(()) }, @@ -350,33 +421,43 @@ impl LookupRangeCheckConfig { element: AssignedCell, num_bits: usize, ) -> Result<(), Error> { - // Enable lookup for `element`, to constrain it to 10 bits. + // Enable lookup for `element`. self.q_lookup.enable(region, 0)?; - // Enable lookup for shifted element, to constrain it to 10 bits. - self.q_lookup.enable(region, 1)?; + match num_bits { + 4 => { + self.q_range_check_4.enable(region, 0)?; + } + 5 => { + self.q_range_check_5.enable(region, 0)?; + } + _ => { + // Enable lookup for shifted element, to constrain it to 10 bits. + self.q_lookup.enable(region, 1)?; - // Check element has been shifted by the correct number of bits. - self.q_bitshift.enable(region, 1)?; + // Check element has been shifted by the correct number of bits. + self.q_bitshift.enable(region, 1)?; - // Assign shifted `element * 2^{K - num_bits}` - let shifted = element.value().into_field() * F::from(1 << (K - num_bits)); + // Assign shifted `element * 2^{K - num_bits}` + let shifted = element.value().into_field() * F::from(1 << (K - num_bits)); - region.assign_advice( - || format!("element * 2^({}-{})", K, num_bits), - self.running_sum, - 1, - || shifted, - )?; + region.assign_advice( + || format!("element * 2^({}-{})", K, num_bits), + self.running_sum, + 1, + || shifted, + )?; - // Assign 2^{-num_bits} from a fixed column. - let inv_two_pow_s = F::from(1 << num_bits).invert().unwrap(); - region.assign_advice_from_constant( - || format!("2^(-{})", num_bits), - self.running_sum, - 2, - inv_two_pow_s, - )?; + // Assign 2^{-num_bits} from a fixed column. + let inv_two_pow_s = F::from(1 << num_bits).invert().unwrap(); + region.assign_advice_from_constant( + || format!("2^(-{})", num_bits), + self.running_sum, + 2, + inv_two_pow_s, + )?; + } + } Ok(()) } @@ -418,10 +499,16 @@ mod tests { fn configure(meta: &mut ConstraintSystem) -> Self::Config { let running_sum = meta.advice_column(); let table_idx = meta.lookup_table_column(); + let table_range_check_tag = meta.lookup_table_column(); let constants = meta.fixed_column(); meta.enable_constant(constants); - LookupRangeCheckConfig::::configure(meta, running_sum, table_idx) + LookupRangeCheckConfig::::configure( + meta, + running_sum, + table_idx, + table_range_check_tag, + ) } fn synthesize( @@ -517,10 +604,16 @@ mod tests { fn configure(meta: &mut ConstraintSystem) -> Self::Config { let running_sum = meta.advice_column(); let table_idx = meta.lookup_table_column(); + let table_range_check_tag = meta.lookup_table_column(); let constants = meta.fixed_column(); meta.enable_constant(constants); - LookupRangeCheckConfig::::configure(meta, running_sum, table_idx) + LookupRangeCheckConfig::::configure( + meta, + running_sum, + table_idx, + table_range_check_tag, + ) } fn synthesize( @@ -646,5 +739,34 @@ mod tests { }]) ); } + + // Element within 4 bits + { + let circuit: MyCircuit = MyCircuit { + element: Value::known(pallas::Base::from((1 << 4) - 1)), + num_bits: 4, + }; + let prover = MockProver::::run(11, &circuit, vec![]).unwrap(); + assert_eq!(prover.verify(), Ok(())); + } + + // Element larger than 5 bits + { + let circuit: MyCircuit = MyCircuit { + element: Value::known(pallas::Base::from(1 << 5)), + num_bits: 5, + }; + let prover = MockProver::::run(11, &circuit, vec![]).unwrap(); + assert_eq!( + prover.verify(), + Err(vec![VerifyFailure::Lookup { + lookup_index: 0, + location: FailureLocation::InRegion { + region: (1, "Range check 5 bits").into(), + offset: 0, + }, + }]) + ); + } } } From f51eebeb4e2a91f7763da63c72208174a4221276 Mon Sep 17 00:00:00 2001 From: Constance Beguier Date: Tue, 17 Oct 2023 09:13:40 +0200 Subject: [PATCH 09/15] Add multiplexer chip (#23) It is now possible to perform a mux between two points or between two non-identity points. `mux(choice, left, right)` will return `left` when `choice=0` and `right` when `choice=1`. `choice` must be constrained to `{0, 1}` outside the gate. It is no longer needed to expose `from_coordinates_unchecked`. --- halo2_gadgets/src/ecc/chip.rs | 2 +- halo2_gadgets/src/utilities.rs | 1 + halo2_gadgets/src/utilities/mux.rs | 450 +++++++++++++++++++++++++++++ 3 files changed, 452 insertions(+), 1 deletion(-) create mode 100644 halo2_gadgets/src/utilities/mux.rs diff --git a/halo2_gadgets/src/ecc/chip.rs b/halo2_gadgets/src/ecc/chip.rs index 119b1670f6..402020384b 100644 --- a/halo2_gadgets/src/ecc/chip.rs +++ b/halo2_gadgets/src/ecc/chip.rs @@ -48,7 +48,7 @@ impl EccPoint { /// Constructs a point from its coordinates, without checking they are on the curve. /// /// This is an internal API that we only use where we know we have a valid curve point. - pub fn from_coordinates_unchecked( + pub(crate) fn from_coordinates_unchecked( x: AssignedCell, pallas::Base>, y: AssignedCell, pallas::Base>, ) -> Self { diff --git a/halo2_gadgets/src/utilities.rs b/halo2_gadgets/src/utilities.rs index 739f4411de..b497ad2361 100644 --- a/halo2_gadgets/src/utilities.rs +++ b/halo2_gadgets/src/utilities.rs @@ -12,6 +12,7 @@ use std::ops::Range; pub mod cond_swap; pub mod decompose_running_sum; pub mod lookup_range_check; +pub mod mux; /// A type that has a value at either keygen or proving time. pub trait FieldValue { diff --git a/halo2_gadgets/src/utilities/mux.rs b/halo2_gadgets/src/utilities/mux.rs new file mode 100644 index 0000000000..82e5422397 --- /dev/null +++ b/halo2_gadgets/src/utilities/mux.rs @@ -0,0 +1,450 @@ +//! Gadget and chip for a multiplexer. +//! +//! Given an input `(choice, left, right)`, the multiplexer returns +//! - `left` if choice=0, +//! - `right` otherwise. +//! `left` and `right` are either both points or both non-identity points. +//! The output of the multiplexer has the same format as the `left` and `right` inputs. +//! If `left` and `right` are points (resp. non-identity points), the output is a point (resp. non-identity point). +//! +//! `choice` must be constrained to {0, 1} separately. + +use crate::ecc::chip::{EccPoint, NonIdentityEccPoint}; +use halo2_proofs::{ + circuit::{AssignedCell, Chip, Layouter, Value}, + plonk::{self, Advice, Column, ConstraintSystem, Constraints, Expression, Selector}, + poly::Rotation, +}; +use pasta_curves::pallas; + +/// Instructions for a multiplexer gadget. +pub trait MuxInstructions { + /// Given an input `(choice, left, right)`, returns `left` if choice=0 and `right` otherwise. + /// + /// `left` and `right` are `EccPoint` + /// `choice` must be constrained to {0, 1} separately. + fn mux_on_points( + &self, + layouter: impl Layouter, + choice: &AssignedCell, + left: &EccPoint, + right: &EccPoint, + ) -> Result; + + /// Given an input `(choice, left, right)`, returns `left` if choice=0 and `right` otherwise. + /// + /// `left` and `right` are `NonIdentityEccPoint` + /// `choice` must be constrained to {0, 1} separately. + fn mux_on_non_identity_points( + &self, + layouter: impl Layouter, + choice: &AssignedCell, + left: &NonIdentityEccPoint, + right: &NonIdentityEccPoint, + ) -> Result; +} + +/// A chip implementing a multiplexer. +#[derive(Clone, Debug)] +pub struct MuxChip { + config: MuxConfig, +} + +impl Chip for MuxChip { + type Config = MuxConfig; + type Loaded = (); + + fn config(&self) -> &Self::Config { + &self.config + } + + fn loaded(&self) -> &Self::Loaded { + &() + } +} + +/// Configuration for the [`MuxChip`]. +#[derive(Clone, Debug)] +pub struct MuxConfig { + choice: Column, + left: Column, + right: Column, + out: Column, + q_mux: Selector, +} + +impl MuxInstructions for MuxChip { + fn mux_on_points( + &self, + mut layouter: impl Layouter, + choice: &AssignedCell, + left: &EccPoint, + right: &EccPoint, + ) -> Result { + let x_cell = layouter.assign_region( + || "mux x", + |mut region| { + self.config.q_mux.enable(&mut region, 0)?; + + choice.copy_advice(|| "copy choice", &mut region, self.config.choice, 0)?; + left.x() + .copy_advice(|| "copy left_x", &mut region, self.config.left, 0)?; + right + .x() + .copy_advice(|| "copy right_x", &mut region, self.config.right, 0)?; + + let out_val = (Value::known(pallas::Base::one()) - choice.value()) + * left.x().value() + + choice.value() * right.x().value(); + + region.assign_advice(|| "out x", self.config.out, 0, || out_val) + }, + )?; + let y_cell = layouter.assign_region( + || "mux y", + |mut region| { + self.config.q_mux.enable(&mut region, 0)?; + + choice.copy_advice(|| "copy choice", &mut region, self.config.choice, 0)?; + left.y() + .copy_advice(|| "copy left_y", &mut region, self.config.left, 0)?; + right + .y() + .copy_advice(|| "copy right_y", &mut region, self.config.right, 0)?; + + let out_val = (Value::known(pallas::Base::one()) - choice.value()) + * left.y().value() + + choice.value() * right.y().value(); + + region.assign_advice(|| "out y", self.config.out, 0, || out_val) + }, + )?; + + Ok(EccPoint::from_coordinates_unchecked( + x_cell.into(), + y_cell.into(), + )) + } + + fn mux_on_non_identity_points( + &self, + mut layouter: impl Layouter, + choice: &AssignedCell, + left: &NonIdentityEccPoint, + right: &NonIdentityEccPoint, + ) -> Result { + let x_cell = layouter.assign_region( + || "mux x", + |mut region| { + self.config.q_mux.enable(&mut region, 0)?; + + choice.copy_advice(|| "copy choice", &mut region, self.config.choice, 0)?; + left.x() + .copy_advice(|| "copy left_x", &mut region, self.config.left, 0)?; + right + .x() + .copy_advice(|| "copy right_x", &mut region, self.config.right, 0)?; + + let out_val = (Value::known(pallas::Base::one()) - choice.value()) + * left.x().value() + + choice.value() * right.x().value(); + + region.assign_advice(|| "out x", self.config.out, 0, || out_val) + }, + )?; + let y_cell = layouter.assign_region( + || "mux y", + |mut region| { + self.config.q_mux.enable(&mut region, 0)?; + + choice.copy_advice(|| "copy choice", &mut region, self.config.choice, 0)?; + left.y() + .copy_advice(|| "copy left_y", &mut region, self.config.left, 0)?; + right + .y() + .copy_advice(|| "copy right_y", &mut region, self.config.right, 0)?; + + let out_val = (Value::known(pallas::Base::one()) - choice.value()) + * left.y().value() + + choice.value() * right.y().value(); + + region.assign_advice(|| "out y", self.config.out, 0, || out_val) + }, + )?; + + Ok(NonIdentityEccPoint::from_coordinates_unchecked( + x_cell.into(), + y_cell.into(), + )) + } +} + +impl MuxChip { + /// Configures this chip for use in a circuit. + pub fn configure( + meta: &mut ConstraintSystem, + choice: Column, + left: Column, + right: Column, + out: Column, + ) -> MuxConfig { + let q_mux = meta.selector(); + meta.create_gate("Field element multiplexer", |meta| { + let q_mux = meta.query_selector(q_mux); + let choice = meta.query_advice(choice, Rotation::cur()); + let left = meta.query_advice(left, Rotation::cur()); + let right = meta.query_advice(right, Rotation::cur()); + let out = meta.query_advice(out, Rotation::cur()); + + let one = Expression::Constant(pallas::Base::one()); + + let should_be_zero = (one - choice.clone()) * left + choice * right - out; + + Constraints::with_selector(q_mux, Some(should_be_zero)) + }); + + MuxConfig { + choice, + left, + right, + out, + q_mux, + } + } + + /// Constructs a [`MuxChip`] given a [`MuxConfig`]. + pub fn construct(config: MuxConfig) -> Self { + Self { config } + } +} + +#[cfg(test)] +mod tests { + use super::{MuxChip, MuxConfig, MuxInstructions}; + + 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, + mux_config: MuxConfig, + ecc_config: EccConfig, + } + #[derive(Default)] + struct MyCircuit { + left_point: Value, + right_point: Value, + choice: Value, + } + + #[test] + fn test_mux_on_points() { + 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 mux_config = + MuxChip::configure(meta, advices[0], advices[1], advices[2], advices[3]); + + 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, + table_range_check_tag, + ); + + let ecc_config = EccChip::::configure( + meta, + advices, + lagrange_coeffs, + range_check, + ); + + MyConfig { + primary, + advice: advices[0], + mux_config, + ecc_config, + } + } + + fn synthesize( + &self, + config: Self::Config, + mut layouter: impl Layouter, + ) -> Result<(), Error> { + // Construct a MUX chip + let mux_chip = MuxChip::construct(config.mux_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 = mux_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 = mux_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(())); + } + } +} From cba30b1b8410d534dac883a045c7368ad4132412 Mon Sep 17 00:00:00 2001 From: Constance Beguier Date: Wed, 18 Oct 2023 09:46:39 +0200 Subject: [PATCH 10/15] Add functions to evaluate a Sinsemilla hash from an initial private point (#22) To share ZEC and ZSA hash computations in Orchard circuit's note commitment evaluation, we need to compute a Sinsemille hash from a private input point. --- halo2_gadgets/src/sinsemilla.rs | 60 ++++- halo2_gadgets/src/sinsemilla/chip.rs | 37 ++- .../src/sinsemilla/chip/hash_to_point.rs | 255 ++++++++++++++---- halo2_gadgets/src/sinsemilla/merkle/chip.rs | 13 + 4 files changed, 313 insertions(+), 52 deletions(-) diff --git a/halo2_gadgets/src/sinsemilla.rs b/halo2_gadgets/src/sinsemilla.rs index a1dcb9d2a2..2a9c92a79c 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; } @@ -329,6 +343,21 @@ 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 @@ -431,6 +460,35 @@ where self.M.hash_to_point(layouter, message) } + #[allow(non_snake_case)] + #[allow(clippy::type_complexity)] + /// $\mathsf{SinsemillaCommit}$ from [§ 5.4.8.4][concretesinsemillacommit]. + /// + /// [concretesinsemillacommit]: https://zips.z.cash/protocol/nu5.pdf#concretesinsemillacommit + 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)] + /// $\mathsf{SinsemillaCommit}$ from [§ 5.4.8.4][concretesinsemillacommit]. + /// + /// [concretesinsemillacommit]: https://zips.z.cash/protocol/nu5.pdf#concretesinsemillacommit + pub fn q_init(&self) -> C { + self.M.Q + } + #[allow(clippy::type_complexity)] /// $\mathsf{SinsemillaCommit}$ from [§ 5.4.8.4][concretesinsemillacommit]. /// diff --git a/halo2_gadgets/src/sinsemilla/chip.rs b/halo2_gadgets/src/sinsemilla/chip.rs index ef76b19815..441ba975af 100644 --- a/halo2_gadgets/src/sinsemilla/chip.rs +++ b/halo2_gadgets/src/sinsemilla/chip.rs @@ -43,8 +43,11 @@ where /// q_sinsemilla2 is used to define a synthetic selector, /// 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$. + /// the y-coordinate of the domain $Q$ when $y_Q$ is a public constant. q_sinsemilla4: Selector, + /// Simple selector used to constrain hash initialization to be consistent with + /// the y-coordinate of the domain $Q$ when $y_Q$ is a private value. + q_sinsemilla4_private: Selector, /// Fixed column used to load the y-coordinate of the domain $Q$. fixed_y_q: Column, /// Logic specific to merged double-and-add. @@ -165,6 +168,7 @@ where q_sinsemilla1: meta.complex_selector(), q_sinsemilla2: meta.fixed_column(), q_sinsemilla4: meta.selector(), + q_sinsemilla4_private: meta.selector(), fixed_y_q, double_and_add: DoubleAndAdd { x_a: advices[0], @@ -202,7 +206,7 @@ where // Check that the initial x_A, x_P, lambda_1, lambda_2 are consistent with y_Q. // https://p.z.cash/halo2-0.1:sinsemilla-constraints?partial - meta.create_gate("Initial y_Q", |meta| { + meta.create_gate("Initial y_Q (public)", |meta| { let q_s4 = meta.query_selector(config.q_sinsemilla4); let y_q = meta.query_fixed(config.fixed_y_q); @@ -215,6 +219,21 @@ where Constraints::with_selector(q_s4, Some(("init_y_q_check", init_y_q_check))) }); + // Check that the initial x_A, x_P, lambda_1, lambda_2 are consistent with y_Q. + // https://p.z.cash/halo2-0.1:sinsemilla-constraints?partial + meta.create_gate("Initial y_Q (private)", |meta| { + let q_s4 = meta.query_selector(config.q_sinsemilla4_private); + let y_q = meta.query_advice(config.double_and_add.x_p, Rotation::prev()); + + // Y_A = (lambda_1 + lambda_2) * (x_a - x_r) + let Y_A_cur = Y_A(meta, Rotation::cur()); + + // 2 * y_q - Y_{A,0} = 0 + let init_y_q_check = y_q * two - Y_A_cur; + + Constraints::with_selector(q_s4, Some(("init_y_q_check", init_y_q_check))) + }); + // https://p.z.cash/halo2-0.1:sinsemilla-constraints?partial meta.create_gate("Sinsemilla gate", |meta| { let q_s1 = meta.query_selector(config.q_sinsemilla1); @@ -322,6 +341,20 @@ where ) } + #[allow(non_snake_case)] + #[allow(clippy::type_complexity)] + fn hash_to_point_with_private_init( + &self, + mut layouter: impl Layouter, + Q: &Self::NonIdentityPoint, + message: Self::Message, + ) -> Result<(Self::NonIdentityPoint, Vec), Error> { + layouter.assign_region( + || "hash_to_point", + |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/hash_to_point.rs b/halo2_gadgets/src/sinsemilla/chip/hash_to_point.rs index 44beaa4259..771d2a9c5f 100644 --- a/halo2_gadgets/src/sinsemilla/chip/hash_to_point.rs +++ b/halo2_gadgets/src/sinsemilla/chip/hash_to_point.rs @@ -41,8 +41,143 @@ where ), Error, > { + 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 + }); + } + + 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(super) 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)?; + + #[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, + )) + } + + #[allow(non_snake_case)] + /// Assign the coordinates of the initial public point `Q` + /// + /// | offset | x_A | q_sinsemilla4 | fixed_y_Q | + /// -------------------------------------------- + /// | 0 | x_Q | 1 | y_Q | + fn public_initialization( + &self, + region: &mut Region<'_, pallas::Base>, + Q: pallas::Affine, + ) -> Result<(usize, X, Y), Error> { let config = self.config().clone(); - let mut offset = 0; + let offset = 0; // Get the `x`- and `y`-coordinates of the starting `Q` base. let x_q = *Q.coordinates().unwrap().x(); @@ -50,7 +185,7 @@ where // Constrain the initial x_a, lambda_1, lambda_2, x_p using the q_sinsemilla4 // selector. - let mut y_a: Y = { + let y_a: Y = { // Enable `q_sinsemilla4` on the first row. config.q_sinsemilla4.enable(region, offset)?; region.assign_fixed( @@ -64,7 +199,7 @@ where }; // Constrain the initial x_q to equal the x-coordinate of the domain's `Q`. - let mut x_a: X = { + let x_a: X = { let x_a = region.assign_advice_from_constant( || "fixed x_q", config.double_and_add.x_a, @@ -75,6 +210,73 @@ where 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_private | + /// ----------------------------------------------- + /// | 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_private selector. + let y_a: Y = { + // Enable `q_sinsemilla4_private` on the first row. + config.q_sinsemilla4_private.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`. + /// + /// Before this call to `hash_all_pieces()`, `x_Q` and `y_Q` MUST have been already assigned + /// within this region. + fn hash_all_pieces( + &self, + region: &mut Region<'_, pallas::Base>, + mut offset: usize, + message: &>::Message, + mut x_a: X, + mut y_a: Y, + ) -> Result< + ( + X, + AssignedCell, pallas::Base>, + Vec>>, + ), + Error, + > { + let config = self.config().clone(); + let mut zs_sum: Vec>> = Vec::new(); // Hash each piece in the message. @@ -121,52 +323,7 @@ where y_a_cell }; - #[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 - }); - } - - 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, - )) + Ok((x_a, y_a, zs_sum)) } #[allow(clippy::type_complexity)] diff --git a/halo2_gadgets/src/sinsemilla/merkle/chip.rs b/halo2_gadgets/src/sinsemilla/merkle/chip.rs index 2c37fe924b..45f0c5f958 100644 --- a/halo2_gadgets/src/sinsemilla/merkle/chip.rs +++ b/halo2_gadgets/src/sinsemilla/merkle/chip.rs @@ -523,6 +523,19 @@ 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) } From 87464d4b3f1eb50e9f9e1e553c14b8a639fbb77f Mon Sep 17 00:00:00 2001 From: Constance Date: Thu, 23 Nov 2023 16:55:48 +0100 Subject: [PATCH 11/15] Update comments --- halo2_gadgets/src/sinsemilla.rs | 17 +++++------------ .../src/sinsemilla/chip/generator_table.rs | 16 ++++++++++++++++ .../src/sinsemilla/chip/hash_to_point.rs | 3 --- .../src/utilities/lookup_range_check.rs | 2 +- 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/halo2_gadgets/src/sinsemilla.rs b/halo2_gadgets/src/sinsemilla.rs index 2a9c92a79c..e57c0a2129 100644 --- a/halo2_gadgets/src/sinsemilla.rs +++ b/halo2_gadgets/src/sinsemilla.rs @@ -442,9 +442,8 @@ where } #[allow(clippy::type_complexity)] - /// $\mathsf{SinsemillaCommit}$ from [§ 5.4.8.4][concretesinsemillacommit]. - /// - /// [concretesinsemillacommit]: https://zips.z.cash/protocol/nu5.pdf#concretesinsemillacommit + /// Evaluates the Sinsemilla hash of `message` from the public initial point `Q` stored + /// into `CommitDomain`. pub fn hash( &self, layouter: impl Layouter, @@ -462,9 +461,7 @@ where #[allow(non_snake_case)] #[allow(clippy::type_complexity)] - /// $\mathsf{SinsemillaCommit}$ from [§ 5.4.8.4][concretesinsemillacommit]. - /// - /// [concretesinsemillacommit]: https://zips.z.cash/protocol/nu5.pdf#concretesinsemillacommit + /// Evaluates the Sinsemilla hash of `message` from the private initial point `Q`. pub fn hash_with_private_init( &self, layouter: impl Layouter, @@ -482,17 +479,13 @@ where } #[allow(clippy::type_complexity)] - /// $\mathsf{SinsemillaCommit}$ from [§ 5.4.8.4][concretesinsemillacommit]. - /// - /// [concretesinsemillacommit]: https://zips.z.cash/protocol/nu5.pdf#concretesinsemillacommit + /// Returns the public initial point `Q` stored into `CommitDomain`. pub fn q_init(&self) -> C { self.M.Q } #[allow(clippy::type_complexity)] - /// $\mathsf{SinsemillaCommit}$ from [§ 5.4.8.4][concretesinsemillacommit]. - /// - /// [concretesinsemillacommit]: https://zips.z.cash/protocol/nu5.pdf#concretesinsemillacommit + /// Evaluates the blinding factor equal to $\[r\] R$ where `r` is stored in the `CommitDomain`. pub fn blinding_factor( &self, mut layouter: impl Layouter, diff --git a/halo2_gadgets/src/sinsemilla/chip/generator_table.rs b/halo2_gadgets/src/sinsemilla/chip/generator_table.rs index 13773a4659..e77928b128 100644 --- a/halo2_gadgets/src/sinsemilla/chip/generator_table.rs +++ b/halo2_gadgets/src/sinsemilla/chip/generator_table.rs @@ -78,6 +78,22 @@ 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", diff --git a/halo2_gadgets/src/sinsemilla/chip/hash_to_point.rs b/halo2_gadgets/src/sinsemilla/chip/hash_to_point.rs index 771d2a9c5f..226e5e8a05 100644 --- a/halo2_gadgets/src/sinsemilla/chip/hash_to_point.rs +++ b/halo2_gadgets/src/sinsemilla/chip/hash_to_point.rs @@ -253,9 +253,6 @@ where #[allow(clippy::type_complexity)] /// Hash `message` from the initial point `Q`. - /// - /// Before this call to `hash_all_pieces()`, `x_Q` and `y_Q` MUST have been already assigned - /// within this region. fn hash_all_pieces( &self, region: &mut Region<'_, pallas::Base>, diff --git a/halo2_gadgets/src/utilities/lookup_range_check.rs b/halo2_gadgets/src/utilities/lookup_range_check.rs index d1173babbc..64b4ce4c90 100644 --- a/halo2_gadgets/src/utilities/lookup_range_check.rs +++ b/halo2_gadgets/src/utilities/lookup_range_check.rs @@ -150,7 +150,7 @@ impl LookupRangeCheckConfig { * q_range_check_4 * Expression::Constant(F::from(4_u64)); - // Combine the running sum and short lookups: + // Combine the running sum, short lookups and optimized range checks: vec![ ( q_lookup.clone() From d76d2317b75e79480eb3c7df4f28428e92643d24 Mon Sep 17 00:00:00 2001 From: Constance Date: Thu, 23 Nov 2023 21:12:08 +0100 Subject: [PATCH 12/15] Reactivate i686 CI test --- .github/workflows/ci.yml | 60 ++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db9abdc62e..7cb0e84e71 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,36 +33,36 @@ jobs: ${{ steps.prepare.outputs.feature-flags }} ${{ matrix.extra_flags }} -# test-32-bit: -# name: Test on i686-unknown-linux-gnu${{ matrix.name_suffix }} -# runs-on: ubuntu-latest -# strategy: -# matrix: -# stage: [stable, beta, nightly] -# include: -# - stage: beta -# name_suffix: " with beta features" -# - stage: nightly -# name_suffix: " with nightly features" -# -# steps: -# - uses: actions/checkout@v3 -# - id: prepare -# uses: ./.github/actions/prepare -# with: -# beta-features: ${{ matrix.stage == 'beta' }} -# nightly-features: ${{ matrix.stage == 'nightly' }} -# - name: Install cross-platform support dependencies -# run: sudo apt install gcc-multilib -# - run: rustup target add i686-unknown-linux-gnu -# - name: Run tests -# run: > -# cargo test -# --verbose -# --release -# --workspace -# --target i686-unknown-linux-gnu -# ${{ steps.prepare.outputs.feature-flags }} + test-32-bit: + name: Test on i686-unknown-linux-gnu${{ matrix.name_suffix }} + runs-on: ubuntu-latest + strategy: + matrix: + stage: [stable, beta, nightly] + include: + - stage: beta + name_suffix: " with beta features" + - stage: nightly + name_suffix: " with nightly features" + + steps: + - uses: actions/checkout@v3 + - id: prepare + uses: ./.github/actions/prepare + with: + beta-features: ${{ matrix.stage == 'beta' }} + nightly-features: ${{ matrix.stage == 'nightly' }} + - name: Install cross-platform support dependencies + run: sudo apt install gcc-multilib + - run: rustup target add i686-unknown-linux-gnu + - name: Run tests + run: > + cargo test + --verbose + --release + --workspace + --target i686-unknown-linux-gnu + ${{ steps.prepare.outputs.feature-flags }} build: name: Build target ${{ matrix.target }} From ac7a90d9ad28a4088b6b843f58a41712ab42a895 Mon Sep 17 00:00:00 2001 From: Constance Date: Tue, 28 Nov 2023 17:13:29 +0100 Subject: [PATCH 13/15] Some minor changes --- halo2_gadgets/src/ecc/chip/witness_point.rs | 4 ++-- halo2_gadgets/src/sinsemilla/primitives.rs | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/halo2_gadgets/src/ecc/chip/witness_point.rs b/halo2_gadgets/src/ecc/chip/witness_point.rs index 28e8aaf4a9..98f865a6dc 100644 --- a/halo2_gadgets/src/ecc/chip/witness_point.rs +++ b/halo2_gadgets/src/ecc/chip/witness_point.rs @@ -102,7 +102,7 @@ impl Config { Ok((x_var, y_var)) } - fn assign_xy_constant( + fn assign_xy_from_constant( &self, value: (Assigned, Assigned), offset: usize, @@ -159,7 +159,7 @@ impl Config { (value.x().into(), value.y().into()) }; - self.assign_xy_constant(value, offset, region) + self.assign_xy_from_constant(value, offset, region) .map(|(x, y)| EccPoint::from_coordinates_unchecked(x, y)) } diff --git a/halo2_gadgets/src/sinsemilla/primitives.rs b/halo2_gadgets/src/sinsemilla/primitives.rs index f90c63ac12..ad9e397b5e 100644 --- a/halo2_gadgets/src/sinsemilla/primitives.rs +++ b/halo2_gadgets/src/sinsemilla/primitives.rs @@ -185,7 +185,7 @@ impl HashDomain { #[allow(non_snake_case)] pub struct CommitDomain { /// A domain in which $\mathsf{SinsemillaHashToPoint}$ and $\mathsf{SinsemillaHash}$ can be used - pub M: HashDomain, + M: HashDomain, R: pallas::Point, } @@ -226,6 +226,13 @@ 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`. From 622875ead0116a0fc60c42279cafc94f1eeee351 Mon Sep 17 00:00:00 2001 From: Constance Date: Thu, 30 Nov 2023 11:17:49 +0100 Subject: [PATCH 14/15] Remove q_sinsemilla4_private --- halo2_gadgets/src/sinsemilla/chip.rs | 23 +--------- .../src/sinsemilla/chip/hash_to_point.rs | 43 ++++++++++--------- 2 files changed, 25 insertions(+), 41 deletions(-) diff --git a/halo2_gadgets/src/sinsemilla/chip.rs b/halo2_gadgets/src/sinsemilla/chip.rs index 441ba975af..c55efd1105 100644 --- a/halo2_gadgets/src/sinsemilla/chip.rs +++ b/halo2_gadgets/src/sinsemilla/chip.rs @@ -43,11 +43,8 @@ where /// q_sinsemilla2 is used to define a synthetic selector, /// 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$ when $y_Q$ is a public constant. + /// the y-coordinate of the domain $Q$. q_sinsemilla4: Selector, - /// Simple selector used to constrain hash initialization to be consistent with - /// the y-coordinate of the domain $Q$ when $y_Q$ is a private value. - q_sinsemilla4_private: Selector, /// Fixed column used to load the y-coordinate of the domain $Q$. fixed_y_q: Column, /// Logic specific to merged double-and-add. @@ -168,7 +165,6 @@ where q_sinsemilla1: meta.complex_selector(), q_sinsemilla2: meta.fixed_column(), q_sinsemilla4: meta.selector(), - q_sinsemilla4_private: meta.selector(), fixed_y_q, double_and_add: DoubleAndAdd { x_a: advices[0], @@ -206,23 +202,8 @@ where // Check that the initial x_A, x_P, lambda_1, lambda_2 are consistent with y_Q. // https://p.z.cash/halo2-0.1:sinsemilla-constraints?partial - meta.create_gate("Initial y_Q (public)", |meta| { + meta.create_gate("Initial y_Q", |meta| { let q_s4 = meta.query_selector(config.q_sinsemilla4); - let y_q = meta.query_fixed(config.fixed_y_q); - - // Y_A = (lambda_1 + lambda_2) * (x_a - x_r) - let Y_A_cur = Y_A(meta, Rotation::cur()); - - // 2 * y_q - Y_{A,0} = 0 - let init_y_q_check = y_q * two - Y_A_cur; - - Constraints::with_selector(q_s4, Some(("init_y_q_check", init_y_q_check))) - }); - - // Check that the initial x_A, x_P, lambda_1, lambda_2 are consistent with y_Q. - // https://p.z.cash/halo2-0.1:sinsemilla-constraints?partial - meta.create_gate("Initial y_Q (private)", |meta| { - let q_s4 = meta.query_selector(config.q_sinsemilla4_private); let y_q = meta.query_advice(config.double_and_add.x_p, Rotation::prev()); // Y_A = (lambda_1 + lambda_2) * (x_a - x_r) diff --git a/halo2_gadgets/src/sinsemilla/chip/hash_to_point.rs b/halo2_gadgets/src/sinsemilla/chip/hash_to_point.rs index 226e5e8a05..165615efaa 100644 --- a/halo2_gadgets/src/sinsemilla/chip/hash_to_point.rs +++ b/halo2_gadgets/src/sinsemilla/chip/hash_to_point.rs @@ -168,16 +168,17 @@ where #[allow(non_snake_case)] /// Assign the coordinates of the initial public point `Q` /// - /// | offset | x_A | q_sinsemilla4 | fixed_y_Q | - /// -------------------------------------------- - /// | 0 | x_Q | 1 | y_Q | + /// | offset | x_A | x_P | q_sinsemilla4 | + /// -------------------------------------- + /// | 0 | | y_Q | | + /// | 1 | x_Q | | 1 | fn public_initialization( &self, region: &mut Region<'_, pallas::Base>, Q: pallas::Affine, ) -> Result<(usize, X, Y), Error> { let config = self.config().clone(); - let offset = 0; + let mut offset = 0; // Get the `x`- and `y`-coordinates of the starting `Q` base. let x_q = *Q.coordinates().unwrap().x(); @@ -186,17 +187,19 @@ where // 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 first row. - config.q_sinsemilla4.enable(region, offset)?; - region.assign_fixed( - || "fixed y_q", - config.fixed_y_q, - offset, - || Value::known(y_q), - )?; + // 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(), + )?; - Value::known(y_q.into()).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 = { @@ -216,10 +219,10 @@ where #[allow(non_snake_case)] /// Assign the coordinates of the initial private point `Q` /// - /// | offset | x_A | x_P | q_sinsemilla4_private | - /// ----------------------------------------------- - /// | 0 | | y_Q | | - /// | 1 | x_Q | | 1 | + /// | offset | x_A | x_P | q_sinsemilla4 | + /// -------------------------------------- + /// | 0 | | y_Q | | + /// | 1 | x_Q | | 1 | fn private_initialization( &self, region: &mut Region<'_, pallas::Base>, @@ -229,10 +232,10 @@ where 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_private selector. + // x_p, y_Q using the q_sinsemilla4 selector. let y_a: Y = { - // Enable `q_sinsemilla4_private` on the first row. - config.q_sinsemilla4_private.enable(region, offset + 1)?; + // 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)?; From 5f436dc3387665fe3201d381791a62a8233b2171 Mon Sep 17 00:00:00 2001 From: Constance Date: Thu, 30 Nov 2023 16:14:15 +0100 Subject: [PATCH 15/15] Move mux functionality into CondSwap chip --- halo2_gadgets/src/sinsemilla/merkle/chip.rs | 12 + halo2_gadgets/src/utilities.rs | 1 - halo2_gadgets/src/utilities/cond_swap.rs | 332 ++++++++++++++- halo2_gadgets/src/utilities/mux.rs | 450 -------------------- 4 files changed, 343 insertions(+), 452 deletions(-) delete mode 100644 halo2_gadgets/src/utilities/mux.rs diff --git a/halo2_gadgets/src/sinsemilla/merkle/chip.rs b/halo2_gadgets/src/sinsemilla/merkle/chip.rs index 45f0c5f958..cb3c5be4cc 100644 --- a/halo2_gadgets/src/sinsemilla/merkle/chip.rs +++ b/halo2_gadgets/src/sinsemilla/merkle/chip.rs @@ -441,6 +441,18 @@ 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 diff --git a/halo2_gadgets/src/utilities.rs b/halo2_gadgets/src/utilities.rs index b497ad2361..739f4411de 100644 --- a/halo2_gadgets/src/utilities.rs +++ b/halo2_gadgets/src/utilities.rs @@ -12,7 +12,6 @@ use std::ops::Range; pub mod cond_swap; pub mod decompose_running_sum; pub mod lookup_range_check; -pub mod mux; /// A type that has a value at either keygen or proving time. pub trait FieldValue { diff --git a/halo2_gadgets/src/utilities/cond_swap.rs b/halo2_gadgets/src/utilities/cond_swap.rs index d733e6c4fb..78049e742a 100644 --- a/halo2_gadgets/src/utilities/cond_swap.rs +++ b/halo2_gadgets/src/utilities/cond_swap.rs @@ -2,12 +2,14 @@ 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::{Advice, Column, ConstraintSystem, Constraints, Error, Selector}, + plonk::{self, Advice, Column, ConstraintSystem, Constraints, Error, Selector}, poly::Rotation, }; +use pasta_curves::pallas; use std::marker::PhantomData; /// Instructions for a conditional swap gadget. @@ -24,6 +26,16 @@ 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. @@ -121,6 +133,97 @@ 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 { @@ -291,4 +394,231 @@ 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, + 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/mux.rs b/halo2_gadgets/src/utilities/mux.rs deleted file mode 100644 index 82e5422397..0000000000 --- a/halo2_gadgets/src/utilities/mux.rs +++ /dev/null @@ -1,450 +0,0 @@ -//! Gadget and chip for a multiplexer. -//! -//! Given an input `(choice, left, right)`, the multiplexer returns -//! - `left` if choice=0, -//! - `right` otherwise. -//! `left` and `right` are either both points or both non-identity points. -//! The output of the multiplexer has the same format as the `left` and `right` inputs. -//! If `left` and `right` are points (resp. non-identity points), the output is a point (resp. non-identity point). -//! -//! `choice` must be constrained to {0, 1} separately. - -use crate::ecc::chip::{EccPoint, NonIdentityEccPoint}; -use halo2_proofs::{ - circuit::{AssignedCell, Chip, Layouter, Value}, - plonk::{self, Advice, Column, ConstraintSystem, Constraints, Expression, Selector}, - poly::Rotation, -}; -use pasta_curves::pallas; - -/// Instructions for a multiplexer gadget. -pub trait MuxInstructions { - /// Given an input `(choice, left, right)`, returns `left` if choice=0 and `right` otherwise. - /// - /// `left` and `right` are `EccPoint` - /// `choice` must be constrained to {0, 1} separately. - fn mux_on_points( - &self, - layouter: impl Layouter, - choice: &AssignedCell, - left: &EccPoint, - right: &EccPoint, - ) -> Result; - - /// Given an input `(choice, left, right)`, returns `left` if choice=0 and `right` otherwise. - /// - /// `left` and `right` are `NonIdentityEccPoint` - /// `choice` must be constrained to {0, 1} separately. - fn mux_on_non_identity_points( - &self, - layouter: impl Layouter, - choice: &AssignedCell, - left: &NonIdentityEccPoint, - right: &NonIdentityEccPoint, - ) -> Result; -} - -/// A chip implementing a multiplexer. -#[derive(Clone, Debug)] -pub struct MuxChip { - config: MuxConfig, -} - -impl Chip for MuxChip { - type Config = MuxConfig; - type Loaded = (); - - fn config(&self) -> &Self::Config { - &self.config - } - - fn loaded(&self) -> &Self::Loaded { - &() - } -} - -/// Configuration for the [`MuxChip`]. -#[derive(Clone, Debug)] -pub struct MuxConfig { - choice: Column, - left: Column, - right: Column, - out: Column, - q_mux: Selector, -} - -impl MuxInstructions for MuxChip { - fn mux_on_points( - &self, - mut layouter: impl Layouter, - choice: &AssignedCell, - left: &EccPoint, - right: &EccPoint, - ) -> Result { - let x_cell = layouter.assign_region( - || "mux x", - |mut region| { - self.config.q_mux.enable(&mut region, 0)?; - - choice.copy_advice(|| "copy choice", &mut region, self.config.choice, 0)?; - left.x() - .copy_advice(|| "copy left_x", &mut region, self.config.left, 0)?; - right - .x() - .copy_advice(|| "copy right_x", &mut region, self.config.right, 0)?; - - let out_val = (Value::known(pallas::Base::one()) - choice.value()) - * left.x().value() - + choice.value() * right.x().value(); - - region.assign_advice(|| "out x", self.config.out, 0, || out_val) - }, - )?; - let y_cell = layouter.assign_region( - || "mux y", - |mut region| { - self.config.q_mux.enable(&mut region, 0)?; - - choice.copy_advice(|| "copy choice", &mut region, self.config.choice, 0)?; - left.y() - .copy_advice(|| "copy left_y", &mut region, self.config.left, 0)?; - right - .y() - .copy_advice(|| "copy right_y", &mut region, self.config.right, 0)?; - - let out_val = (Value::known(pallas::Base::one()) - choice.value()) - * left.y().value() - + choice.value() * right.y().value(); - - region.assign_advice(|| "out y", self.config.out, 0, || out_val) - }, - )?; - - Ok(EccPoint::from_coordinates_unchecked( - x_cell.into(), - y_cell.into(), - )) - } - - fn mux_on_non_identity_points( - &self, - mut layouter: impl Layouter, - choice: &AssignedCell, - left: &NonIdentityEccPoint, - right: &NonIdentityEccPoint, - ) -> Result { - let x_cell = layouter.assign_region( - || "mux x", - |mut region| { - self.config.q_mux.enable(&mut region, 0)?; - - choice.copy_advice(|| "copy choice", &mut region, self.config.choice, 0)?; - left.x() - .copy_advice(|| "copy left_x", &mut region, self.config.left, 0)?; - right - .x() - .copy_advice(|| "copy right_x", &mut region, self.config.right, 0)?; - - let out_val = (Value::known(pallas::Base::one()) - choice.value()) - * left.x().value() - + choice.value() * right.x().value(); - - region.assign_advice(|| "out x", self.config.out, 0, || out_val) - }, - )?; - let y_cell = layouter.assign_region( - || "mux y", - |mut region| { - self.config.q_mux.enable(&mut region, 0)?; - - choice.copy_advice(|| "copy choice", &mut region, self.config.choice, 0)?; - left.y() - .copy_advice(|| "copy left_y", &mut region, self.config.left, 0)?; - right - .y() - .copy_advice(|| "copy right_y", &mut region, self.config.right, 0)?; - - let out_val = (Value::known(pallas::Base::one()) - choice.value()) - * left.y().value() - + choice.value() * right.y().value(); - - region.assign_advice(|| "out y", self.config.out, 0, || out_val) - }, - )?; - - Ok(NonIdentityEccPoint::from_coordinates_unchecked( - x_cell.into(), - y_cell.into(), - )) - } -} - -impl MuxChip { - /// Configures this chip for use in a circuit. - pub fn configure( - meta: &mut ConstraintSystem, - choice: Column, - left: Column, - right: Column, - out: Column, - ) -> MuxConfig { - let q_mux = meta.selector(); - meta.create_gate("Field element multiplexer", |meta| { - let q_mux = meta.query_selector(q_mux); - let choice = meta.query_advice(choice, Rotation::cur()); - let left = meta.query_advice(left, Rotation::cur()); - let right = meta.query_advice(right, Rotation::cur()); - let out = meta.query_advice(out, Rotation::cur()); - - let one = Expression::Constant(pallas::Base::one()); - - let should_be_zero = (one - choice.clone()) * left + choice * right - out; - - Constraints::with_selector(q_mux, Some(should_be_zero)) - }); - - MuxConfig { - choice, - left, - right, - out, - q_mux, - } - } - - /// Constructs a [`MuxChip`] given a [`MuxConfig`]. - pub fn construct(config: MuxConfig) -> Self { - Self { config } - } -} - -#[cfg(test)] -mod tests { - use super::{MuxChip, MuxConfig, MuxInstructions}; - - 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, - mux_config: MuxConfig, - ecc_config: EccConfig, - } - #[derive(Default)] - struct MyCircuit { - left_point: Value, - right_point: Value, - choice: Value, - } - - #[test] - fn test_mux_on_points() { - 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 mux_config = - MuxChip::configure(meta, advices[0], advices[1], advices[2], advices[3]); - - 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, - table_range_check_tag, - ); - - let ecc_config = EccChip::::configure( - meta, - advices, - lagrange_coeffs, - range_check, - ); - - MyConfig { - primary, - advice: advices[0], - mux_config, - ecc_config, - } - } - - fn synthesize( - &self, - config: Self::Config, - mut layouter: impl Layouter, - ) -> Result<(), Error> { - // Construct a MUX chip - let mux_chip = MuxChip::construct(config.mux_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 = mux_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 = mux_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(())); - } - } -}