From ae253103856508cfb9f0ee15a6de3194b072b045 Mon Sep 17 00:00:00 2001 From: therealyingtong Date: Sat, 5 Jun 2021 14:34:44 +0800 Subject: [PATCH] chip::mul_fixed.rs: Implement fixed-base scalar mul instruction. Fixed-base scalar mul makes use of the add_incomplete and add instructions internally. The full-width and short signed share some common logic, which is captured in chip::mul_fixed.rs. The signed short variant introduces additional logic to handle the scalar's sign. This is done in the submodule mul_fixed::short. --- src/circuit/gadget/ecc/chip.rs | 42 +- src/circuit/gadget/ecc/chip/mul_fixed.rs | 529 ++++++++++++++++++ .../gadget/ecc/chip/mul_fixed/full_width.rs | 171 ++++++ .../gadget/ecc/chip/mul_fixed/short.rs | 246 ++++++++ 4 files changed, 979 insertions(+), 9 deletions(-) create mode 100644 src/circuit/gadget/ecc/chip/mul_fixed.rs create mode 100644 src/circuit/gadget/ecc/chip/mul_fixed/full_width.rs create mode 100644 src/circuit/gadget/ecc/chip/mul_fixed/short.rs diff --git a/src/circuit/gadget/ecc/chip.rs b/src/circuit/gadget/ecc/chip.rs index b297df14..c3e29cfd 100644 --- a/src/circuit/gadget/ecc/chip.rs +++ b/src/circuit/gadget/ecc/chip.rs @@ -13,7 +13,7 @@ use pasta_curves::{arithmetic::CurveAffine, pallas}; pub(super) mod add; pub(super) mod add_incomplete; pub(super) mod mul; -// pub(super) mod mul_fixed; +pub(super) mod mul_fixed; pub(super) mod witness_point; pub(super) mod witness_scalar_fixed; @@ -199,6 +199,20 @@ impl EccChip { config.create_gate(meta); } + // Create fixed-base scalar mul gate that os used in both full-width + // and short multiplication. + { + let mul_fixed_config: mul_fixed::Config<{ constants::NUM_WINDOWS }> = (&config).into(); + mul_fixed_config.create_gate(meta); + } + + // Create gate that is only used in short fixed-base scalar mul. + { + let short_config: mul_fixed::short::Config<{ constants::NUM_WINDOWS_SHORT }> = + (&config).into(); + short_config.create_gate(meta); + } + config } } @@ -354,19 +368,29 @@ impl EccInstructions for EccChip { fn mul_fixed( &self, - _layouter: &mut impl Layouter, - _scalar: &Self::ScalarFixed, - _base: &Self::FixedPoints, + layouter: &mut impl Layouter, + scalar: &Self::ScalarFixed, + base: &Self::FixedPoints, ) -> Result { - todo!() + let config: mul_fixed::full_width::Config<{ constants::NUM_WINDOWS }> = + self.config().into(); + layouter.assign_region( + || format!("fixed-base mul of {:?}", base), + |mut region| config.assign_region(scalar, *base, 0, &mut region), + ) } fn mul_fixed_short( &self, - _layouter: &mut impl Layouter, - _scalar: &Self::ScalarFixedShort, - _base: &Self::FixedPointsShort, + layouter: &mut impl Layouter, + scalar: &Self::ScalarFixedShort, + base: &Self::FixedPointsShort, ) -> Result { - todo!() + let config: mul_fixed::short::Config<{ constants::NUM_WINDOWS_SHORT }> = + self.config().into(); + layouter.assign_region( + || format!("short fixed-base mul of {:?}", base), + |mut region| config.assign_region(scalar, base, 0, &mut region), + ) } } diff --git a/src/circuit/gadget/ecc/chip/mul_fixed.rs b/src/circuit/gadget/ecc/chip/mul_fixed.rs new file mode 100644 index 00000000..b2c5b577 --- /dev/null +++ b/src/circuit/gadget/ecc/chip/mul_fixed.rs @@ -0,0 +1,529 @@ +use std::array; + +use super::{ + add, add_incomplete, copy, CellValue, EccConfig, EccPoint, EccScalarFixed, EccScalarFixedShort, + Var, +}; +use crate::constants::{ + self, + load::{OrchardFixedBase, OrchardFixedBasesFull, ValueCommitV, WindowUs}, +}; + +use group::Curve; +use halo2::{ + circuit::Region, + plonk::{Advice, Column, ConstraintSystem, Error, Expression, Fixed, Permutation, Selector}, + poly::Rotation, +}; +use pasta_curves::{ + arithmetic::{CurveAffine, FieldExt}, + pallas, +}; + +pub mod full_width; +pub mod short; + +// A sum type for both full-width and short bases. This enables us to use the +// shared functionality of full-width and short fixed-base scalar multiplication. +#[derive(Copy, Clone, Debug)] +enum OrchardFixedBases { + Full(OrchardFixedBasesFull), + ValueCommitV, +} + +impl From for OrchardFixedBases { + fn from(full_width_base: OrchardFixedBasesFull) -> Self { + Self::Full(full_width_base) + } +} + +impl From for OrchardFixedBases { + fn from(_value_commit_v: ValueCommitV) -> Self { + Self::ValueCommitV + } +} + +impl OrchardFixedBases { + pub fn generator(self) -> pallas::Affine { + match self { + Self::ValueCommitV => constants::value_commit_v::generator(), + Self::Full(base) => { + let base: OrchardFixedBase = base.into(); + base.generator + } + } + } + + pub fn u(self) -> Vec { + match self { + Self::ValueCommitV => ValueCommitV::get().u_short.0.as_ref().to_vec(), + Self::Full(base) => { + let base: OrchardFixedBase = base.into(); + base.u.0.as_ref().to_vec() + } + } + } +} + +#[derive(Clone, Debug)] +pub struct Config { + // Selector used in both short and full-width fixed-base scalar mul. + q_mul_fixed: Selector, + + // The fixed Lagrange interpolation coefficients for `x_p`. + lagrange_coeffs: [Column; constants::H], + // The fixed `z` for each window such that `y + z = u^2`. + fixed_z: Column, + // Decomposition of an `n-1`-bit scalar into `k`-bit windows: + // a = a_0 + 2^k(a_1) + 2^{2k}(a_2) + ... + 2^{(n-1)k}(a_{n-1}) + window: Column, + // x-coordinate of the multiple of the fixed base at the current window. + x_p: Column, + // y-coordinate of the multiple of the fixed base at the current window. + y_p: Column, + // y-coordinate of accumulator (only used in the final row). + u: Column, + // Permutation + perm: Permutation, + // Configuration for `add` + add_config: add::Config, + // Configuration for `add_incomplete` + add_incomplete_config: add_incomplete::Config, +} + +impl From<&EccConfig> for Config { + fn from(ecc_config: &EccConfig) -> Self { + let config = Self { + q_mul_fixed: ecc_config.q_mul_fixed, + lagrange_coeffs: ecc_config.lagrange_coeffs, + fixed_z: ecc_config.fixed_z, + x_p: ecc_config.advices[0], + y_p: ecc_config.advices[1], + window: ecc_config.advices[4], + u: ecc_config.advices[5], + perm: ecc_config.perm.clone(), + add_config: ecc_config.into(), + add_incomplete_config: ecc_config.into(), + }; + + // Check relationships between this config and `add_config`. + assert_eq!( + config.x_p, config.add_config.x_p, + "add is used internally in mul_fixed." + ); + assert_eq!( + config.y_p, config.add_config.y_p, + "add is used internally in mul_fixed." + ); + + // Check relationships between this config and `add_incomplete_config`. + assert_eq!( + config.x_p, config.add_incomplete_config.x_p, + "add_incomplete is used internally in mul_fixed." + ); + assert_eq!( + config.y_p, config.add_incomplete_config.y_p, + "add_incomplete is used internally in mul_fixed." + ); + for advice in [config.x_p, config.y_p, config.window, config.u].iter() { + assert_ne!( + *advice, config.add_config.x_qr, + "Do not overlap with output columns of add." + ); + assert_ne!( + *advice, config.add_config.y_qr, + "Do not overlap with output columns of add." + ); + } + + config + } +} + +impl Config { + #[allow(clippy::op_ref)] + pub(super) fn create_gate(&self, meta: &mut ConstraintSystem) { + meta.create_gate("Fixed-base scalar mul gate", |meta| { + let q_mul_fixed = meta.query_selector(self.q_mul_fixed); + let y_p = meta.query_advice(self.y_p, Rotation::cur()); + + let window = meta.query_advice(self.window, Rotation::cur()); + let x_p = meta.query_advice(self.x_p, Rotation::cur()); + + let z = meta.query_fixed(self.fixed_z, Rotation::cur()); + let u = meta.query_advice(self.u, Rotation::cur()); + + let window_pow: Vec> = (0..constants::H) + .map(|pow| { + (0..pow).fold(Expression::Constant(pallas::Base::one()), |acc, _| { + acc * window.clone() + }) + }) + .collect(); + + let interpolated_x = window_pow.iter().zip(self.lagrange_coeffs.iter()).fold( + Expression::Constant(pallas::Base::zero()), + |acc, (window_pow, coeff)| { + acc + (window_pow.clone() * meta.query_fixed(*coeff, Rotation::cur())) + }, + ); + + // Check interpolation of x-coordinate + let x_check = interpolated_x - x_p; + // Check that `y + z = u^2`, where `z` is fixed and `u`, `y` are witnessed + let y_check = u.clone() * u - y_p - z; + + array::IntoIter::new([x_check, y_check]).map(move |poly| q_mul_fixed.clone() * poly) + }); + } + + #[allow(clippy::type_complexity)] + fn assign_region_inner( + &self, + region: &mut Region<'_, pallas::Base>, + offset: usize, + scalar: &ScalarFixed, + base: OrchardFixedBases, + ) -> Result<(EccPoint, EccPoint), Error> { + // Assign fixed columns for given fixed base + self.assign_fixed_constants(region, offset, base)?; + + // Copy the scalar decomposition + self.copy_scalar(region, offset, scalar)?; + + // Initialize accumulator + let acc = self.initialize_accumulator(region, offset, base, scalar)?; + + // Process all windows excluding least and most significant windows + let acc = self.add_incomplete(region, offset, acc, base, scalar)?; + + // Process most significant window using complete addition + let mul_b = self.process_msb(region, offset, base, scalar)?; + + Ok((acc, mul_b)) + } + + fn assign_fixed_constants( + &self, + region: &mut Region<'_, pallas::Base>, + offset: usize, + base: OrchardFixedBases, + ) -> Result<(), Error> { + let (lagrange_coeffs, z) = match base { + OrchardFixedBases::ValueCommitV => { + assert_eq!(NUM_WINDOWS, constants::NUM_WINDOWS_SHORT); + let base = ValueCommitV::get(); + ( + base.lagrange_coeffs_short.0.as_ref().to_vec(), + base.z_short.0.as_ref().to_vec(), + ) + } + OrchardFixedBases::Full(base) => { + assert_eq!(NUM_WINDOWS, constants::NUM_WINDOWS); + let base: OrchardFixedBase = base.into(); + ( + base.lagrange_coeffs.0.as_ref().to_vec(), + base.z.0.as_ref().to_vec(), + ) + } + }; + + // Assign fixed columns for given fixed base + for window in 0..NUM_WINDOWS { + // Enable `q_mul_fixed` selector + self.q_mul_fixed.enable(region, window + offset)?; + + // Assign x-coordinate Lagrange interpolation coefficients + for k in 0..(constants::H) { + region.assign_fixed( + || { + format!( + "Lagrange interpolation coeff for window: {:?}, k: {:?}", + window, k + ) + }, + self.lagrange_coeffs[k], + window + offset, + || Ok(lagrange_coeffs[window].0[k]), + )?; + } + + // Assign z-values for each window + region.assign_fixed( + || format!("z-value for window: {:?}", window), + self.fixed_z, + window + offset, + || Ok(z[window]), + )?; + } + + Ok(()) + } + + fn copy_scalar( + &self, + region: &mut Region<'_, pallas::Base>, + offset: usize, + scalar: &ScalarFixed, + ) -> Result<(), Error> { + // Copy the scalar decomposition (`k`-bit windows) + for (window_idx, window) in scalar.windows().iter().enumerate() { + copy( + region, + || format!("k[{:?}]", window), + self.window, + window_idx + offset, + window, + &self.perm, + )?; + } + + Ok(()) + } + + fn initialize_accumulator( + &self, + region: &mut Region<'_, pallas::Base>, + offset: usize, + base: OrchardFixedBases, + scalar: &ScalarFixed, + ) -> Result { + // Recall that the message at each window `w` is represented as + // `m_w = [(k_w + 2) ⋅ 8^w]B`. + // When `w = 0`, we have `m_0 = [(k_0 + 2)]B`. + let m0 = { + let k0 = scalar.windows_field()[0]; + let m0 = k0.map(|k0| base.generator() * (k0 + pallas::Scalar::from_u64(2))); + let m0 = m0.map(|m0| m0.to_affine().coordinates().unwrap()); + + let x = m0.map(|m0| *m0.x()); + let x_cell = region.assign_advice( + || "m0_x", + self.x_p, + offset, + || x.ok_or(Error::SynthesisError), + )?; + let x = CellValue::new(x_cell, x); + + let y = m0.map(|m0| *m0.y()); + let y_cell = region.assign_advice( + || "m0_y", + self.y_p, + offset, + || y.ok_or(Error::SynthesisError), + )?; + let y = CellValue::new(y_cell, y); + + EccPoint { x, y } + }; + + // Assign u = (y_p + z_w).sqrt() for `m0` + { + let k0 = scalar.windows_usize()[0]; + let u0 = &base.u()[0]; + let u0 = k0.map(|k0| u0.0[k0]); + + region.assign_advice(|| "u", self.u, offset, || u0.ok_or(Error::SynthesisError))?; + } + + // Copy `m0` into `x_qr`, `y_qr` cells on row 1 of the incomplete addition. + let x = copy( + region, + || "initialize acc x", + self.add_incomplete_config.x_qr, + offset + 1, + &m0.x, + &self.perm, + )?; + let y = copy( + region, + || "initialize acc y", + self.add_incomplete_config.y_qr, + offset + 1, + &m0.y, + &self.perm, + )?; + + Ok(EccPoint { x, y }) + } + + fn add_incomplete( + &self, + region: &mut Region<'_, pallas::Base>, + offset: usize, + mut acc: EccPoint, + base: OrchardFixedBases, + scalar: &ScalarFixed, + ) -> Result { + // This is 2^w, where w is the window width + let h = pallas::Scalar::from_u64(constants::H as u64); + + let base_value = base.generator(); + let base_u = base.u(); + let scalar_windows_field = scalar.windows_field(); + let scalar_windows_usize = scalar.windows_usize(); + + for (w, k) in scalar_windows_field[1..(scalar_windows_field.len() - 1)] + .iter() + .enumerate() + { + // Offset window index by 1 since we are starting on k_1 + let w = w + 1; + + // Compute [(k_w + 2) ⋅ 8^w]B + let mul_b = { + let mul_b = k.map(|k| { + base_value * (k + pallas::Scalar::from_u64(2)) * h.pow(&[w as u64, 0, 0, 0]) + }); + let mul_b = mul_b.map(|mul_b| mul_b.to_affine().coordinates().unwrap()); + + let x = mul_b.map(|mul_b| *mul_b.x()); + let x_cell = region.assign_advice( + || format!("mul_b_x, window {}", w), + self.x_p, + offset + w, + || x.ok_or(Error::SynthesisError), + )?; + let x = CellValue::new(x_cell, x); + + let y = mul_b.map(|mul_b| *mul_b.y()); + let y_cell = region.assign_advice( + || format!("mul_b_y, window {}", w), + self.y_p, + offset + w, + || y.ok_or(Error::SynthesisError), + )?; + let y = CellValue::new(y_cell, y); + + EccPoint { x, y } + }; + + // Assign u = (y_p + z_w).sqrt() + let u_val = scalar_windows_usize[w].map(|k| base_u[w].0[k]); + region.assign_advice( + || "u", + self.u, + offset + w, + || u_val.ok_or(Error::SynthesisError), + )?; + + // Add to the accumulator + acc = self + .add_incomplete_config + .assign_region(&mul_b, &acc, offset + w, region)?; + } + Ok(acc) + } + + fn process_msb( + &self, + region: &mut Region<'_, pallas::Base>, + offset: usize, + base: OrchardFixedBases, + scalar: &ScalarFixed, + ) -> Result { + // This is 2^w, where w is the window width + let h = pallas::Scalar::from_u64(constants::H as u64); + + // Assign u = (y_p + z_w).sqrt() for the most significant window + { + let u_val = + scalar.windows_usize()[NUM_WINDOWS - 1].map(|k| base.u()[NUM_WINDOWS - 1].0[k]); + region.assign_advice( + || "u", + self.u, + offset + NUM_WINDOWS - 1, + || u_val.ok_or(Error::SynthesisError), + )?; + } + + // offset_acc = \sum_{j = 0}^{NUM_WINDOWS - 2} 2^{FIXED_BASE_WINDOW_SIZE * j+1} + let offset_acc = (0..(NUM_WINDOWS - 1)).fold(pallas::Scalar::zero(), |acc, w| { + acc + pallas::Scalar::from_u64(2).pow(&[ + constants::FIXED_BASE_WINDOW_SIZE as u64 * w as u64 + 1, + 0, + 0, + 0, + ]) + }); + + // `scalar = [k * 8^84 - offset_acc]`, where `offset_acc = \sum_{j = 0}^{83} 2^{FIXED_BASE_WINDOW_SIZE * j + 1}`. + let scalar = scalar.windows_field()[scalar.windows_field().len() - 1] + .map(|k| k * h.pow(&[(NUM_WINDOWS - 1) as u64, 0, 0, 0]) - offset_acc); + + let mul_b = { + let mul_b = scalar.map(|scalar| base.generator() * scalar); + let mul_b = mul_b.map(|mul_b| mul_b.to_affine().coordinates().unwrap()); + + let x = mul_b.map(|mul_b| *mul_b.x()); + let x_cell = region.assign_advice( + || format!("mul_b_x, window {}", NUM_WINDOWS - 1), + self.x_p, + offset + NUM_WINDOWS - 1, + || x.ok_or(Error::SynthesisError), + )?; + let x = CellValue::new(x_cell, x); + + let y = mul_b.map(|mul_b| *mul_b.y()); + let y_cell = region.assign_advice( + || format!("mul_b_y, window {}", NUM_WINDOWS - 1), + self.y_p, + offset + NUM_WINDOWS - 1, + || y.ok_or(Error::SynthesisError), + )?; + let y = CellValue::new(y_cell, y); + + EccPoint { x, y } + }; + + Ok(mul_b) + } +} + +enum ScalarFixed { + FullWidth(EccScalarFixed), + Short(EccScalarFixedShort), +} + +impl From<&EccScalarFixed> for ScalarFixed { + fn from(scalar_fixed: &EccScalarFixed) -> Self { + Self::FullWidth(scalar_fixed.clone()) + } +} + +impl From<&EccScalarFixedShort> for ScalarFixed { + fn from(scalar_fixed: &EccScalarFixedShort) -> Self { + Self::Short(scalar_fixed.clone()) + } +} + +impl ScalarFixed { + fn windows(&self) -> &[CellValue] { + match self { + ScalarFixed::FullWidth(scalar) => &scalar.windows, + ScalarFixed::Short(scalar) => &scalar.windows, + } + } + + // The scalar decomposition was done in the base field. For computation + // outside the circuit, we now convert them back into the scalar field. + fn windows_field(&self) -> Vec> { + self.windows() + .iter() + .map(|bits| { + bits.value() + .map(|value| pallas::Scalar::from_bytes(&value.to_bytes()).unwrap()) + }) + .collect::>() + } + + // The scalar decomposition is guaranteed to be in three-bit windows, + // so we also cast the least significant byte in their serialisation + // into usize for convenient indexing into `u`-values + fn windows_usize(&self) -> Vec> { + self.windows() + .iter() + .map(|bits| bits.value().map(|value| value.to_bytes()[0] as usize)) + .collect::>() + } +} diff --git a/src/circuit/gadget/ecc/chip/mul_fixed/full_width.rs b/src/circuit/gadget/ecc/chip/mul_fixed/full_width.rs new file mode 100644 index 00000000..92cf5495 --- /dev/null +++ b/src/circuit/gadget/ecc/chip/mul_fixed/full_width.rs @@ -0,0 +1,171 @@ +use super::super::{EccConfig, EccPoint, EccScalarFixed, OrchardFixedBasesFull}; + +use halo2::{circuit::Region, plonk::Error}; +use pasta_curves::pallas; + +pub struct Config(super::Config); + +impl From<&EccConfig> for Config { + fn from(config: &EccConfig) -> Self { + Self(config.into()) + } +} + +impl Config { + pub fn assign_region( + &self, + scalar: &EccScalarFixed, + base: OrchardFixedBasesFull, + offset: usize, + region: &mut Region<'_, pallas::Base>, + ) -> Result { + let (acc, mul_b) = + self.0 + .assign_region_inner(region, offset, &scalar.into(), base.into())?; + + // Add to the accumulator and return the final result as `[scalar]B`. + let result = self + .0 + .add_config + .assign_region(&mul_b, &acc, offset + NUM_WINDOWS, region)?; + + #[cfg(test)] + // Check that the correct multiple is obtained. + { + use group::Curve; + use halo2::arithmetic::FieldExt; + + let base: super::OrchardFixedBases = base.into(); + let scalar = scalar + .value + .map(|scalar| pallas::Scalar::from_bytes(&scalar.to_bytes()).unwrap()); + let real_mul = scalar.map(|scalar| base.generator() * scalar); + let result = result.point(); + + if let (Some(real_mul), Some(result)) = (real_mul, result) { + assert_eq!(real_mul.to_affine(), result); + } + } + + Ok(result) + } +} + +#[cfg(test)] +pub mod tests { + use ff::PrimeFieldBits; + use halo2::{circuit::Layouter, plonk::Error}; + use pasta_curves::{arithmetic::FieldExt, pallas}; + + use crate::circuit::gadget::ecc::{ + chip::{EccChip, OrchardFixedBasesFull}, + FixedPoint, ScalarFixed, + }; + use crate::constants; + + pub fn test_mul_fixed( + chip: EccChip, + mut layouter: impl Layouter, + ) -> Result<(), Error> { + // commit_ivk_r + let commit_ivk_r = OrchardFixedBasesFull::CommitIvkR; + let commit_ivk_r = FixedPoint::from_inner(chip.clone(), commit_ivk_r); + test_single_base( + chip.clone(), + layouter.namespace(|| "commit_ivk_r"), + commit_ivk_r, + )?; + + // note_commit_r + let note_commit_r = OrchardFixedBasesFull::NoteCommitR; + let note_commit_r = FixedPoint::from_inner(chip.clone(), note_commit_r); + test_single_base( + chip.clone(), + layouter.namespace(|| "note_commit_r"), + note_commit_r, + )?; + + // nullifier_k + let nullifier_k = OrchardFixedBasesFull::NullifierK; + let nullifier_k = FixedPoint::from_inner(chip.clone(), nullifier_k); + test_single_base( + chip.clone(), + layouter.namespace(|| "nullifier_k"), + nullifier_k, + )?; + + // value_commit_r + let value_commit_r = OrchardFixedBasesFull::ValueCommitR; + let value_commit_r = FixedPoint::from_inner(chip.clone(), value_commit_r); + test_single_base( + chip.clone(), + layouter.namespace(|| "value_commit_r"), + value_commit_r, + )?; + + // spend_auth_g + let spend_auth_g = OrchardFixedBasesFull::SpendAuthG; + let spend_auth_g = FixedPoint::from_inner(chip.clone(), spend_auth_g); + test_single_base(chip, layouter.namespace(|| "spend_auth_g"), spend_auth_g)?; + + Ok(()) + } + + #[allow(clippy::op_ref)] + fn test_single_base( + chip: EccChip, + mut layouter: impl Layouter, + base: FixedPoint, + ) -> Result<(), Error> + where + pallas::Scalar: PrimeFieldBits, + { + // [a]B + { + let scalar_fixed = pallas::Scalar::rand(); + + let scalar_fixed = ScalarFixed::new( + chip.clone(), + layouter.namespace(|| "ScalarFixed"), + Some(scalar_fixed), + )?; + + base.mul(layouter.namespace(|| "mul"), &scalar_fixed)?; + } + + // There is a single canonical sequence of window values for which a doubling occurs on the last step: + // 1333333333333333333333333333333333333333333333333333333333333333333333333333333333334 in octal. + // (There is another *non-canonical* sequence + // 5333333333333333333333333333333333333333332711161673731021062440252244051273333333333 in octal.) + { + let h = pallas::Scalar::from_u64(constants::H as u64); + let scalar_fixed = "1333333333333333333333333333333333333333333333333333333333333333333333333333333333334" + .chars() + .fold(pallas::Scalar::zero(), |acc, c| { + acc * &h + &pallas::Scalar::from_u64(c.to_digit(8).unwrap().into()) + }); + + let scalar_fixed = ScalarFixed::new( + chip.clone(), + layouter.namespace(|| "ScalarFixed"), + Some(scalar_fixed), + )?; + + base.mul(layouter.namespace(|| "mul with double"), &scalar_fixed)?; + } + + // [0]B should return (0,0) since it uses complete addition + // on the last step. + { + let scalar_fixed = pallas::Scalar::zero(); + let scalar_fixed = ScalarFixed::new( + chip, + layouter.namespace(|| "ScalarFixed"), + Some(scalar_fixed), + )?; + base.mul(layouter.namespace(|| "mul by zero"), &scalar_fixed)?; + } + + Ok(()) + } +} diff --git a/src/circuit/gadget/ecc/chip/mul_fixed/short.rs b/src/circuit/gadget/ecc/chip/mul_fixed/short.rs new file mode 100644 index 00000000..b003a569 --- /dev/null +++ b/src/circuit/gadget/ecc/chip/mul_fixed/short.rs @@ -0,0 +1,246 @@ +use std::array; + +use super::super::{copy, CellValue, EccConfig, EccPoint, EccScalarFixedShort, Var}; +use crate::constants::ValueCommitV; + +use halo2::{ + circuit::Region, + plonk::{ConstraintSystem, Error, Selector}, + poly::Rotation, +}; +use pasta_curves::pallas; + +pub struct Config { + // Selector used for fixed-base scalar mul with short signed exponent. + q_mul_fixed_short: Selector, + super_config: super::Config, +} + +impl From<&EccConfig> for Config { + fn from(config: &EccConfig) -> Self { + Self { + q_mul_fixed_short: config.q_mul_fixed_short, + super_config: config.into(), + } + } +} + +impl Config { + // We reuse the constraints in the `mul_fixed` gate so exclude them here. + // Here, we add some new constraints specific to the short signed case. + pub(crate) fn create_gate(&self, meta: &mut ConstraintSystem) { + meta.create_gate("Short fixed-base mul gate", |meta| { + let q_mul_fixed_short = meta.query_selector(self.q_mul_fixed_short); + let y_p = meta.query_advice(self.super_config.y_p, Rotation::cur()); + let y_a = meta.query_advice(self.super_config.add_config.y_qr, Rotation::cur()); + let sign = meta.query_advice(self.super_config.window, Rotation::cur()); + + // `(x_a, y_a)` is the result of `[m]B`, where `m` is the magnitude. + // We conditionally negate this result using `y_p = y_a * s`, where `s` is the sign. + + // Check that the final `y_p = y_a` or `y_p = -y_a` + let y_check = q_mul_fixed_short.clone() + * (y_p.clone() - y_a.clone()) + * (y_p.clone() + y_a.clone()); + + // Check that the correct sign is witnessed s.t. sign * y_p = y_a + let negation_check = sign * y_p - y_a; + + array::IntoIter::new([y_check, negation_check]) + .map(move |poly| q_mul_fixed_short.clone() * poly) + }); + } + + pub fn assign_region( + &self, + scalar: &EccScalarFixedShort, + base: &ValueCommitV, + offset: usize, + region: &mut Region<'_, pallas::Base>, + ) -> Result { + let (acc, mul_b) = self.super_config.assign_region_inner( + region, + offset, + &scalar.into(), + base.clone().into(), + )?; + + // Add to the cumulative sum to get `[magnitude]B`. + let magnitude_mul = self.super_config.add_config.assign_region( + &mul_b, + &acc, + offset + NUM_WINDOWS, + region, + )?; + + // Increase offset by 1 after complete addition + let offset = offset + 1; + + // Assign sign to `window` column + let sign = copy( + region, + || "sign", + self.super_config.window, + offset + NUM_WINDOWS, + &scalar.sign, + &self.super_config.perm, + )?; + + // Conditionally negate `y`-coordinate + let y_val = if let Some(sign) = sign.value() { + if sign == -pallas::Base::one() { + magnitude_mul.y.value().map(|y: pallas::Base| -y) + } else { + magnitude_mul.y.value() + } + } else { + None + }; + + // Enable mul_fixed_short selector on final row + self.q_mul_fixed_short + .enable(region, offset + NUM_WINDOWS)?; + + // Assign final `x, y` to `x_p, y_p` columns and return final point + let x_val = magnitude_mul.x.value(); + let x_var = region.assign_advice( + || "x_var", + self.super_config.x_p, + offset + NUM_WINDOWS, + || x_val.ok_or(Error::SynthesisError), + )?; + let y_var = region.assign_advice( + || "y_var", + self.super_config.y_p, + offset + NUM_WINDOWS, + || y_val.ok_or(Error::SynthesisError), + )?; + + let result = EccPoint { + x: CellValue::new(x_var, x_val), + y: CellValue::new(y_var, y_val), + }; + + #[cfg(test)] + // Check that the correct multiple is obtained. + { + use group::Curve; + + let base: super::OrchardFixedBases = base.clone().into(); + + let scalar = scalar + .magnitude + .zip(scalar.sign.value()) + .map(|(magnitude, sign)| { + let sign = if sign == pallas::Base::one() { + pallas::Scalar::one() + } else if sign == -pallas::Base::one() { + -pallas::Scalar::one() + } else { + panic!("Sign should be 1 or -1.") + }; + magnitude * sign + }); + let real_mul = scalar.map(|scalar| base.generator() * scalar); + let result = result.point(); + + if let (Some(real_mul), Some(result)) = (real_mul, result) { + assert_eq!(real_mul.to_affine(), result); + } + } + + Ok(result) + } +} + +#[cfg(test)] +pub mod tests { + use ff::PrimeFieldBits; + use halo2::{circuit::Layouter, plonk::Error}; + use pasta_curves::{arithmetic::FieldExt, pallas}; + + use crate::circuit::gadget::ecc::{chip::EccChip, FixedPointShort, ScalarFixedShort}; + use crate::constants::load::ValueCommitV; + + #[allow(clippy::op_ref)] + pub fn test_mul_fixed_short( + chip: EccChip, + mut layouter: impl Layouter, + ) -> Result<(), Error> + where + pallas::Scalar: PrimeFieldBits, + { + // value_commit_v + let value_commit_v = ValueCommitV::get(); + let value_commit_v = FixedPointShort::from_inner(chip.clone(), value_commit_v); + + // [0]B should return (0,0) since it uses complete addition + // on the last step. + { + let scalar_fixed = pallas::Scalar::zero(); + let scalar_fixed = ScalarFixedShort::new( + chip.clone(), + layouter.namespace(|| "ScalarFixedShort"), + Some(scalar_fixed), + )?; + value_commit_v.mul(layouter.namespace(|| "mul"), &scalar_fixed)?; + } + + // Random [a]B + { + let scalar_fixed_short = pallas::Scalar::from_u64(rand::random::()); + let mut sign = pallas::Scalar::one(); + if rand::random::() { + sign = -sign; + } + let scalar_fixed_short = sign * &scalar_fixed_short; + + let scalar_fixed_short = ScalarFixedShort::new( + chip.clone(), + layouter.namespace(|| "ScalarFixedShort"), + Some(scalar_fixed_short), + )?; + value_commit_v.mul(layouter.namespace(|| "mul fixed"), &scalar_fixed_short)?; + } + + // [2^64 - 1]B + { + let scalar_fixed_short = pallas::Scalar::from_u64(0xFFFF_FFFF_FFFF_FFFFu64); + + let scalar_fixed_short = ScalarFixedShort::new( + chip.clone(), + layouter.namespace(|| "ScalarFixedShort"), + Some(scalar_fixed_short), + )?; + value_commit_v.mul(layouter.namespace(|| "mul fixed"), &scalar_fixed_short)?; + } + + // [-(2^64 - 1)]B + { + let scalar_fixed_short = -pallas::Scalar::from_u64(0xFFFF_FFFF_FFFF_FFFFu64); + + let scalar_fixed_short = ScalarFixedShort::new( + chip.clone(), + layouter.namespace(|| "ScalarFixedShort"), + Some(scalar_fixed_short), + )?; + value_commit_v.mul(layouter.namespace(|| "mul fixed"), &scalar_fixed_short)?; + } + + // There is a single canonical sequence of window values for which a doubling occurs on the last step: + // 1333333333333333333334 in octal. + // [0xB6DB_6DB6_DB6D_B6DC] B + { + let scalar_fixed_short = pallas::Scalar::from_u64(0xB6DB_6DB6_DB6D_B6DCu64); + + let scalar_fixed_short = ScalarFixedShort::new( + chip, + layouter.namespace(|| "ScalarFixedShort"), + Some(scalar_fixed_short), + )?; + value_commit_v.mul(layouter.namespace(|| "mul fixed"), &scalar_fixed_short)?; + } + + Ok(()) + } +}