diff --git a/src/circuit/gadget/ecc.rs b/src/circuit/gadget/ecc.rs index 31d252e32..86abd9b55 100644 --- a/src/circuit/gadget/ecc.rs +++ b/src/circuit/gadget/ecc.rs @@ -9,6 +9,8 @@ use halo2::{ plonk::Error, }; +pub mod chip; + /// The set of circuit instructions required to use the ECC gadgets. pub trait EccInstructions: Chip { /// Variable representing an element of the elliptic curve's base field, that @@ -36,16 +38,10 @@ pub trait EccInstructions: Chip { /// Variable representing the affine short Weierstrass x-coordinate of an /// elliptic curve point. type X: Clone + Debug; - /// Variable representing the set of fixed bases in the circuit. + /// Enumeration of the set of fixed bases to be used in full-width scalar mul. type FixedPoints: Clone + Debug; - /// Variable representing the set of fixed bases to be used in scalar - /// multiplication with a short signed exponent. + /// Enumeration of the set of fixed bases to be used in short signed scalar mul. type FixedPointsShort: Clone + Debug; - /// Variable representing a fixed elliptic curve point (constant in the circuit). - type FixedPoint: Clone + Debug; - /// Variable representing a fixed elliptic curve point (constant in the circuit) - /// to be used in scalar multiplication with a short signed exponent. - type FixedPointShort: Clone + Debug; /// Witnesses the given base field element as a private input to the circuit /// for variable-base scalar mul. @@ -81,18 +77,6 @@ pub trait EccInstructions: Chip { /// Extracts the x-coordinate of a point. fn extract_p(point: &Self::Point) -> &Self::X; - /// Returns a fixed point that had been previously loaded into the circuit. - /// The pre-loaded cells are used to set up equality constraints in other - /// parts of the circuit where the fixed base is used. - fn get_fixed(&self, fixed_points: Self::FixedPoints) -> Result; - - /// Returns a fixed point to be used in scalar multiplication with a signed - /// short exponent. - fn get_fixed_short( - &self, - fixed_points: Self::FixedPointsShort, - ) -> Result; - /// Performs incomplete point addition, returning `a + b`. /// /// This returns an error in exceptional cases. @@ -111,14 +95,8 @@ pub trait EccInstructions: Chip { b: &Self::Point, ) -> Result; - /// Performs point doubling, returning `[2] a`. - fn double( - &self, - layouter: &mut impl Layouter, - a: &Self::Point, - ) -> Result; - /// Performs variable-base scalar multiplication, returning `[scalar] base`. + /// Multiplication of the identity `[scalar] 𝒪 ` returns an error. fn mul( &self, layouter: &mut impl Layouter, @@ -131,7 +109,7 @@ pub trait EccInstructions: Chip { &self, layouter: &mut impl Layouter, scalar: &Self::ScalarFixed, - base: &Self::FixedPoint, + base: &Self::FixedPoints, ) -> Result; /// Performs fixed-base scalar multiplication using a short signed scalar, returning `[scalar] base`. @@ -139,7 +117,7 @@ pub trait EccInstructions: Chip { &self, layouter: &mut impl Layouter, scalar: &Self::ScalarFixedShort, - base: &Self::FixedPointShort, + base: &Self::FixedPointsShort, ) -> Result; } @@ -174,12 +152,18 @@ impl + Clone + Debug + Eq> ScalarVar /// A full-width element of the given elliptic curve's scalar field, to be used for fixed-base scalar mul. #[derive(Debug)] -pub struct ScalarFixed + Clone + Debug + Eq> { +pub struct ScalarFixed +where + EccChip: EccInstructions + Clone + Debug + Eq, +{ chip: EccChip, inner: EccChip::ScalarFixed, } -impl + Clone + Debug + Eq> ScalarFixed { +impl ScalarFixed +where + EccChip: EccInstructions + Clone + Debug + Eq, +{ /// Constructs a new ScalarFixed with the given value. pub fn new( chip: EccChip, @@ -193,13 +177,17 @@ impl + Clone + Debug + Eq> ScalarFix /// A signed short element of the given elliptic curve's scalar field, to be used for fixed-base scalar mul. #[derive(Debug)] -pub struct ScalarFixedShort + Clone + Debug + Eq> { +pub struct ScalarFixedShort +where + EccChip: EccInstructions + Clone + Debug + Eq, +{ chip: EccChip, inner: EccChip::ScalarFixedShort, } -impl + Clone + Debug + Eq> - ScalarFixedShort +impl ScalarFixedShort +where + EccChip: EccInstructions + Clone + Debug + Eq, { /// Constructs a new ScalarFixedShort with the given value. /// @@ -231,7 +219,7 @@ impl + Clone + Debug + Eq> } /// An elliptic curve point over the given curve. -#[derive(Debug)] +#[derive(Copy, Clone, Debug)] pub struct Point + Clone + Debug + Eq> { chip: EccChip, inner: EccChip::Point, @@ -318,18 +306,18 @@ impl + Clone + Debug + Eq> X + Clone + Debug + Eq> { +pub struct FixedPoint +where + EccChip: EccInstructions + Clone + Debug + Eq, +{ chip: EccChip, - inner: EccChip::FixedPoint, + inner: EccChip::FixedPoints, } -impl + Clone + Debug + Eq> FixedPoint { - /// Gets a reference to the specified fixed point in the circuit. - pub fn get(chip: EccChip, point: EccChip::FixedPoints) -> Result { - chip.get_fixed(point) - .map(|inner| FixedPoint { chip, inner }) - } - +impl FixedPoint +where + EccChip: EccInstructions + Clone + Debug + Eq, +{ /// Returns `[by] self`. pub fn mul( &self, @@ -344,23 +332,28 @@ impl + Clone + Debug + Eq> FixedPoin inner, }) } + + /// Wraps the given fixed base (obtained directly from an instruction) in a gadget. + pub fn from_inner(chip: EccChip, inner: EccChip::FixedPoints) -> Self { + FixedPoint { chip, inner } + } } /// A constant elliptic curve point over the given curve, used in scalar multiplication /// with a short signed exponent #[derive(Clone, Debug)] -pub struct FixedPointShort + Clone + Debug + Eq> { +pub struct FixedPointShort +where + EccChip: EccInstructions + Clone + Debug + Eq, +{ chip: EccChip, - inner: EccChip::FixedPointShort, + inner: EccChip::FixedPointsShort, } -impl + Clone + Debug + Eq> FixedPointShort { - /// Gets a reference to the specified fixed point in the circuit. - pub fn get(chip: EccChip, point: EccChip::FixedPointsShort) -> Result { - chip.get_fixed_short(point) - .map(|inner| FixedPointShort { chip, inner }) - } - +impl FixedPointShort +where + EccChip: EccInstructions + Clone + Debug + Eq, +{ /// Returns `[by] self`. pub fn mul( &self, @@ -375,4 +368,145 @@ impl + Clone + Debug + Eq> FixedPoin inner, }) } + + /// Wraps the given fixed base (obtained directly from an instruction) in a gadget. + pub fn from_inner(chip: EccChip, inner: EccChip::FixedPointsShort) -> Self { + FixedPointShort { chip, inner } + } +} + +#[cfg(test)] +mod tests { + use group::{Curve, Group}; + use halo2::{ + arithmetic::CurveAffine, + circuit::{layouter::SingleChipLayouter, Layouter}, + dev::MockProver, + pasta::pallas, + plonk::{Assignment, Circuit, ConstraintSystem, Error}, + }; + + use super::chip::{EccChip, EccConfig}; + + struct MyCircuit { + _marker: std::marker::PhantomData, + } + + #[allow(non_snake_case)] + impl Circuit for MyCircuit { + type Config = EccConfig; + + 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 perm = meta.permutation( + &advices + .iter() + .map(|advice| (*advice).into()) + .collect::>(), + ); + + EccChip::::configure(meta, advices, perm) + } + + fn synthesize( + &self, + cs: &mut impl Assignment, + config: Self::Config, + ) -> Result<(), Error> { + let mut layouter = SingleChipLayouter::new(cs)?; + let chip = EccChip::construct(config); + + // Generate a random point P + let p_val = C::CurveExt::random(rand::rngs::OsRng).to_affine(); // P + let p = super::Point::new(chip.clone(), layouter.namespace(|| "point"), Some(p_val))?; + let p_neg = -p_val; + let p_neg = + super::Point::new(chip.clone(), layouter.namespace(|| "point"), Some(p_neg))?; + + // Generate a random point Q + let q_val = C::CurveExt::random(rand::rngs::OsRng).to_affine(); // P + let q = super::Point::new(chip.clone(), layouter.namespace(|| "point"), Some(q_val))?; + + // Make sure P and Q are not the same point. + assert_ne!(p_val, q_val); + + // Generate a (0,0) point to be used in other tests. + let zero = p.add(layouter.namespace(|| "P + (-P)"), &p_neg)?; + + // Test complete addition + { + super::chip::add::tests::test_add( + chip.clone(), + layouter.namespace(|| "complete addition"), + &zero, + p_val, + &p, + q_val, + &q, + &p_neg, + )?; + } + + // Test incomplete addition + { + super::chip::add_incomplete::tests::test_add_incomplete( + layouter.namespace(|| "incomplete addition"), + &zero, + &p, + &q, + &p_neg, + )?; + } + + // Test variable-base scalar multiplication + { + super::chip::mul::tests::test_mul( + chip.clone(), + layouter.namespace(|| "variable-base scalar mul"), + &zero, + &p, + )?; + } + + // Test full-width fixed-base scalar multiplication + { + super::chip::mul_fixed::full_width::tests::test_mul_fixed( + chip.clone(), + layouter.namespace(|| "full-width fixed-base scalar mul"), + )?; + } + + // Test signed short fixed-base scalar multiplication + { + super::chip::mul_fixed::short::tests::test_mul_fixed_short( + chip, + layouter.namespace(|| "signed short fixed-base scalar mul"), + )?; + } + + Ok(()) + } + } + + #[test] + fn ecc() { + let k = 12; + let circuit = MyCircuit:: { + _marker: std::marker::PhantomData, + }; + let prover = MockProver::run(k, &circuit, vec![]).unwrap(); + assert_eq!(prover.verify(), Ok(())) + } } diff --git a/src/circuit/gadget/ecc/chip.rs b/src/circuit/gadget/ecc/chip.rs new file mode 100644 index 000000000..cf99c33e3 --- /dev/null +++ b/src/circuit/gadget/ecc/chip.rs @@ -0,0 +1,358 @@ +use super::EccInstructions; +use crate::circuit::gadget::utilities::{copy, CellValue, Var}; +use crate::constants::{self, OrchardFixedBasesFull, ValueCommitV}; +use arrayvec::ArrayVec; +use ff::Field; +use halo2::{ + arithmetic::CurveAffine, + circuit::{Chip, Layouter}, + plonk::{Advice, Column, ConstraintSystem, Error, Fixed, Permutation, Selector}, +}; +use std::marker::PhantomData; + +pub(super) mod add; +pub(super) mod add_incomplete; +pub(super) mod mul; +pub(super) mod mul_fixed; +pub(super) mod witness_point; +pub(super) mod witness_scalar_fixed; + +/// A curve point represented in affine (x, y) coordinates. Each coordinate is +/// assigned to a cell. +#[derive(Clone, Debug)] +pub struct EccPoint { + /// x-coordinate + pub x: CellValue, + /// y-coordinate + pub y: CellValue, +} + +impl EccPoint { + /// Returns the value of this curve point, if known. + pub fn point(&self) -> Option { + match (self.x.value(), self.y.value()) { + (Some(x), Some(y)) => { + if x == C::Base::zero() && y == C::Base::zero() { + Some(C::identity()) + } else { + Some(C::from_xy(x, y).unwrap()) + } + } + _ => None, + } + } +} + +/// Configuration for the ECC chip +#[derive(Clone, Debug, Eq, PartialEq)] +#[allow(non_snake_case)] +pub struct EccConfig { + /// Advice columns needed by instructions in the ECC chip. + pub advices: [Column; 10], + + /// Coefficients of interpolation polynomials for x-coordinates (used in fixed-base scalar multiplication) + pub lagrange_coeffs: [Column; constants::H], + /// Fixed z such that y + z = u^2 some square, and -y + z is a non-square. (Used in fixed-base scalar multiplication) + pub fixed_z: Column, + + /// Incomplete addition + pub q_add_incomplete: Selector, + /// Complete addition + pub q_add: Selector, + /// Variable-base scalar multiplication (hi half) + pub q_mul_hi: Selector, + /// Variable-base scalar multiplication (lo half) + pub q_mul_lo: Selector, + /// Selector used in scalar decomposition for variable-base scalar mul + pub q_mul_decompose_var: Selector, + /// Variable-base scalar multiplication (final scalar) + pub q_mul_complete: Selector, + /// Fixed-base full-width scalar multiplication + pub q_mul_fixed: Selector, + /// Fixed-base signed short scalar multiplication + pub q_mul_fixed_short: Selector, + /// Witness point + pub q_point: Selector, + /// Witness full-width scalar for fixed-base scalar mul + pub q_scalar_fixed: Selector, + /// Witness signed short scalar for full-width fixed-base scalar mul + pub q_scalar_fixed_short: Selector, + /// Permutation + pub perm: Permutation, +} + +/// A chip implementing EccInstructions +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct EccChip { + config: EccConfig, + _marker: PhantomData, +} + +impl Chip for EccChip { + type Config = EccConfig; + type Loaded = (); + + fn config(&self) -> &Self::Config { + &self.config + } + + fn loaded(&self) -> &Self::Loaded { + &() + } +} + +impl EccChip { + pub fn construct(config: >::Config) -> Self { + Self { + config, + _marker: PhantomData, + } + } + + #[allow(non_snake_case)] + pub fn configure( + meta: &mut ConstraintSystem, + advices: [Column; 10], + perm: Permutation, + ) -> >::Config { + let config = EccConfig { + advices, + 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(), + ], + fixed_z: meta.fixed_column(), + q_add_incomplete: meta.selector(), + q_add: meta.selector(), + q_mul_hi: meta.selector(), + q_mul_lo: meta.selector(), + q_mul_decompose_var: meta.selector(), + q_mul_complete: meta.selector(), + q_mul_fixed: meta.selector(), + q_mul_fixed_short: meta.selector(), + q_point: meta.selector(), + q_scalar_fixed: meta.selector(), + q_scalar_fixed_short: meta.selector(), + perm, + }; + + // Create witness point gate + { + let config: witness_point::Config = (&config).into(); + config.create_gate::(meta); + } + + // Create witness scalar_fixed gates that apply to both full-width and + // short scalars + { + let config: witness_scalar_fixed::Config = (&config).into(); + config.create_gate(meta); + } + + // Create witness scalar_fixed gates that only apply to short scalars + { + let config: witness_scalar_fixed::short::Config = (&config).into(); + config.create_gate(meta); + } + + // Create incomplete point addition gate + { + let config: add_incomplete::Config = (&config).into(); + config.create_gate(meta); + } + + // Create complete point addition gate + { + let add_config: add::Config = (&config).into(); + add_config.create_gate(meta); + } + + // Create fixed-base scalar mul gates that are used in both full-width + // and short multiplication. + { + let mul_fixed_config: mul_fixed::Config = + (&config).into(); + mul_fixed_config.create_gate(meta); + } + + // Create gates that are only used in short fixed-base scalar mul. + { + let short_config: mul_fixed::short::Config = + (&config).into(); + short_config.create_gate(meta); + } + + // Create variable-base scalar mul gates + { + let mul_config: mul::Config = (&config).into(); + mul_config.create_gate(meta); + } + + config + } +} + +/// A full-width scalar used for fixed-base scalar multiplication. +/// This is decomposed in chunks of `window_width` bits in little-endian order. +/// For example, if `window_width` = 3, we will have [k_0, k_1, ..., k_n] +/// where `scalar = k_0 + k_1 * (2^3) + ... + k_n * (2^3)^n` and each `k_i` is +/// in the range [0..2^3). +#[derive(Clone, Debug)] +pub struct EccScalarFixed { + value: Option, + windows: ArrayVec, { constants::NUM_WINDOWS }>, +} + +/// A signed short scalar used for fixed-base scalar multiplication. +/// This is decomposed in chunks of `window_width` bits in little-endian order. +/// For example, if `window_width` = 3, we will have [k_0, k_1, ..., k_n] +/// where `scalar = k_0 + k_1 * (2^3) + ... + k_n * (2^3)^n` and each `k_i` is +/// in the range [0..2^3). +#[derive(Clone, Debug)] +pub struct EccScalarFixedShort { + magnitude: Option, + sign: CellValue, + windows: ArrayVec, { constants::NUM_WINDOWS_SHORT }>, +} + +impl EccInstructions for EccChip { + type ScalarFixed = EccScalarFixed; + type ScalarFixedShort = EccScalarFixedShort; + type ScalarVar = CellValue; + type Point = EccPoint; + type X = CellValue; + type FixedPoints = OrchardFixedBasesFull; + type FixedPointsShort = ValueCommitV; + + fn witness_scalar_var( + &self, + layouter: &mut impl Layouter, + value: Option, + ) -> Result { + layouter.assign_region( + || "Witness scalar for variable-base mul", + |mut region| { + let cell = region.assign_advice( + || "Scalar var", + self.config().advices[0], + 0, + || value.ok_or(Error::SynthesisError), + )?; + Ok(CellValue::new(cell, value)) + }, + ) + } + + fn witness_scalar_fixed( + &self, + layouter: &mut impl Layouter, + value: Option, + ) -> Result { + let config: witness_scalar_fixed::full_width::Config = self.config().into(); + layouter.assign_region( + || "witness scalar for fixed-base mul", + |mut region| config.assign_region(value, 0, &mut region), + ) + } + + fn witness_scalar_fixed_short( + &self, + layouter: &mut impl Layouter, + value: Option, + ) -> Result { + let config: witness_scalar_fixed::short::Config = self.config().into(); + layouter.assign_region( + || "witness short scalar for fixed-base mul", + |mut region| config.assign_region(value, 0, &mut region), + ) + } + + fn witness_point( + &self, + layouter: &mut impl Layouter, + value: Option, + ) -> Result { + let config: witness_point::Config = self.config().into(); + layouter.assign_region( + || "witness point", + |mut region| config.assign_region(value, 0, &mut region), + ) + } + + fn extract_p(point: &Self::Point) -> &Self::X { + &point.x + } + + fn add_incomplete( + &self, + layouter: &mut impl Layouter, + a: &Self::Point, + b: &Self::Point, + ) -> Result { + let config: add_incomplete::Config = self.config().into(); + layouter.assign_region( + || "incomplete point addition", + |mut region| config.assign_region(a, b, 0, &mut region), + ) + } + + fn add( + &self, + layouter: &mut impl Layouter, + a: &Self::Point, + b: &Self::Point, + ) -> Result { + let config: add::Config = self.config().into(); + layouter.assign_region( + || "complete point addition", + |mut region| config.assign_region(a, b, 0, &mut region), + ) + } + + fn mul( + &self, + layouter: &mut impl Layouter, + scalar: &Self::ScalarVar, + base: &Self::Point, + ) -> Result { + let config: mul::Config = self.config().into(); + layouter.assign_region( + || "variable-base mul", + |mut region| config.assign_region(scalar, base, 0, &mut region), + ) + } + + fn mul_fixed( + &self, + layouter: &mut impl Layouter, + scalar: &Self::ScalarFixed, + base: &Self::FixedPoints, + ) -> Result { + let config: mul_fixed::full_width::Config = + 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, + ) -> Result { + let config: mul_fixed::short::Config = + 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/add.rs b/src/circuit/gadget/ecc/chip/add.rs new file mode 100644 index 000000000..7ffc9e5f5 --- /dev/null +++ b/src/circuit/gadget/ecc/chip/add.rs @@ -0,0 +1,469 @@ +use super::{copy, CellValue, EccConfig, EccPoint, Var}; +use ff::Field; +use halo2::{ + arithmetic::{CurveAffine, FieldExt}, + circuit::Region, + plonk::{Advice, Column, ConstraintSystem, Error, Expression, Permutation, Selector}, + poly::Rotation, +}; +use std::collections::HashSet; + +#[derive(Clone, Debug)] +pub struct Config { + q_add: Selector, + // lambda + lambda: Column, + // x-coordinate of P in P + Q = R + pub x_p: Column, + // y-coordinate of P in P + Q = R + pub y_p: Column, + // x-coordinate of Q or R in P + Q = R + pub x_qr: Column, + // y-coordinate of Q or R in P + Q = R + pub y_qr: Column, + // α = inv0(x_q - x_p) + alpha: Column, + // β = inv0(x_p) + beta: Column, + // γ = inv0(x_q) + gamma: Column, + // δ = inv0(y_p + y_q) if x_q = x_p, 0 otherwise + delta: Column, + // Permutation + perm: Permutation, +} + +impl From<&EccConfig> for Config { + fn from(ecc_config: &EccConfig) -> Self { + Self { + q_add: ecc_config.q_add, + x_p: ecc_config.advices[0], + y_p: ecc_config.advices[1], + x_qr: ecc_config.advices[2], + y_qr: ecc_config.advices[3], + lambda: ecc_config.advices[4], + alpha: ecc_config.advices[5], + beta: ecc_config.advices[6], + gamma: ecc_config.advices[7], + delta: ecc_config.advices[8], + perm: ecc_config.perm.clone(), + } + } +} + +impl Config { + pub(crate) fn advice_columns(&self) -> HashSet> { + core::array::IntoIter::new([ + self.x_p, + self.y_p, + self.x_qr, + self.y_qr, + self.lambda, + self.alpha, + self.beta, + self.gamma, + self.delta, + ]) + .collect() + } + + pub(crate) fn create_gate(&self, meta: &mut ConstraintSystem) { + meta.create_gate("complete addition gates", |meta| { + let q_add = meta.query_selector(self.q_add, Rotation::cur()); + let x_p = meta.query_advice(self.x_p, Rotation::cur()); + let y_p = meta.query_advice(self.y_p, Rotation::cur()); + let x_q = meta.query_advice(self.x_qr, Rotation::cur()); + let y_q = meta.query_advice(self.y_qr, Rotation::cur()); + let x_r = meta.query_advice(self.x_qr, Rotation::next()); + let y_r = meta.query_advice(self.y_qr, Rotation::next()); + let lambda = meta.query_advice(self.lambda, Rotation::cur()); + + // α = inv0(x_q - x_p) + let alpha = meta.query_advice(self.alpha, Rotation::cur()); + // β = inv0(x_p) + let beta = meta.query_advice(self.beta, Rotation::cur()); + // γ = inv0(x_q) + let gamma = meta.query_advice(self.gamma, Rotation::cur()); + // δ = inv0(y_p + y_q) if x_q = x_p, 0 otherwise + let delta = meta.query_advice(self.delta, Rotation::cur()); + + // Useful composite expressions + // α ⋅(x_q - x_p) + let if_alpha = (x_q.clone() - x_p.clone()) * alpha; + // β ⋅ x_p + let if_beta = x_p.clone() * beta; + // γ ⋅ x_q + let if_gamma = x_q.clone() * gamma; + // δ ⋅(y_p + y_q) + let if_delta = (y_q.clone() + y_p.clone()) * delta; + + // Useful constants + let one = Expression::Constant(F::one()); + let two = Expression::Constant(F::from_u64(2)); + let three = Expression::Constant(F::from_u64(3)); + + // (x_q − x_p)⋅((x_q − x_p)⋅λ − (y_q−y_p)) = 0 + let poly1 = { + let x_q_minus_x_p = x_q.clone() - x_p.clone(); // (x_q − x_p) + + let y_q_minus_y_p = y_q.clone() - y_p.clone(); // (y_q − y_p) + let incomplete = x_q_minus_x_p.clone() * lambda.clone() - y_q_minus_y_p; // (x_q − x_p)⋅λ − (y_q−y_p) + + // q_add ⋅(x_q − x_p)⋅((x_q − x_p)⋅λ − (y_q−y_p)) + x_q_minus_x_p * incomplete + }; + + // (1 - (x_q - x_p)⋅α)⋅(2y_p ⋅λ - 3x_p^2) = 0 + let poly2 = { + let three_x_p_sq = three * x_p.clone() * x_p.clone(); // 3x_p^2 + let two_y_p = two * y_p.clone(); // 2y_p + let tangent_line = two_y_p * lambda.clone() - three_x_p_sq; // (2y_p ⋅λ - 3x_p^2) + + // q_add ⋅(1 - (x_q - x_p)⋅α)⋅(2y_p ⋅λ - 3x_p^2) + (one.clone() - if_alpha.clone()) * tangent_line + }; + + // x_p⋅x_q⋅(x_q - x_p)⋅(λ^2 - x_p - x_q - x_r) = 0 + let poly3 = { + let x_q_minus_x_p = x_q.clone() - x_p.clone(); // (x_q - x_p) + let secant_line = + lambda.clone() * lambda.clone() - x_p.clone() - x_q.clone() - x_r.clone(); // (λ^2 - x_p - x_q - x_r) + + // x_p⋅x_q⋅(x_q - x_p)⋅(λ^2 - x_p - x_q - x_r) + x_p.clone() * x_q.clone() * x_q_minus_x_p * secant_line + }; + + // x_p⋅x_q⋅(x_q - x_p)⋅(λ ⋅(x_p - x_r) - y_p - y_r) = 0 + let poly4 = { + let x_q_minus_x_p = x_q.clone() - x_p.clone(); // (x_q - x_p) + let x_p_minus_x_r = x_p.clone() - x_r.clone(); // (x_p - x_r) + + // x_p⋅x_q⋅(x_q - x_p)⋅(λ ⋅(x_p - x_r) - y_p - y_r) + x_p.clone() + * x_q.clone() + * x_q_minus_x_p + * (lambda.clone() * x_p_minus_x_r - y_p.clone() - y_r.clone()) + }; + + // x_p⋅x_q⋅(y_q + y_p)⋅(λ^2 - x_p - x_q - x_r) = 0 + let poly5 = { + let y_q_plus_y_p = y_q.clone() + y_p.clone(); // (y_q + y_p) + let output_line_x = + lambda.clone() * lambda.clone() - x_p.clone() - x_q.clone() - x_r.clone(); // (λ^2 - x_p - x_q - x_r) + + // x_p⋅x_q⋅(y_q + y_p)⋅(λ^2 - x_p - x_q - x_r) + x_p.clone() * x_q.clone() * y_q_plus_y_p * output_line_x + }; + + // x_p⋅x_q⋅(y_q + y_p)⋅(λ ⋅(x_p - x_r) - y_p - y_r) = 0 + let poly6 = { + let y_q_plus_y_p = y_q.clone() + y_p.clone(); // (y_q + y_p) + let x_p_minus_x_r = x_p.clone() - x_r.clone(); // (x_p - x_r) + + // x_p⋅x_q⋅(y_q + y_p)⋅(λ ⋅(x_p - x_r) - y_p - y_r) + x_p.clone() + * x_q.clone() + * y_q_plus_y_p + * (lambda * x_p_minus_x_r - y_p.clone() - y_r.clone()) + }; + + // (1 - x_p * β) * (x_r - x_q) = 0 + let poly7 = (one.clone() - if_beta.clone()) * (x_r.clone() - x_q); + + // (1 - x_p * β) * (y_r - y_q) = 0 + let poly8 = (one.clone() - if_beta) * (y_r.clone() - y_q); + + // (1 - x_q * γ) * (x_r - x_p) = 0 + let poly9 = (one.clone() - if_gamma.clone()) * (x_r.clone() - x_p); + + // (1 - x_q * γ) * (y_r - y_p) = 0 + let poly10 = (one.clone() - if_gamma) * (y_r.clone() - y_p); + + // ((1 - (x_q - x_p) * α - (y_q + y_p) * δ)) * x_r + let poly11 = (one.clone() - if_alpha.clone() - if_delta.clone()) * x_r; + + // ((1 - (x_q - x_p) * α - (y_q + y_p) * δ)) * y_r + let poly12 = (one - if_alpha - if_delta) * y_r; + + [ + poly1, poly2, poly3, poly4, poly5, poly6, poly7, poly8, poly9, poly10, poly11, + poly12, + ] + .iter() + .map(|poly| q_add.clone() * poly.clone()) + .collect() + }); + } + + pub(super) fn assign_region( + &self, + p: &EccPoint, + q: &EccPoint, + offset: usize, + region: &mut Region<'_, C::Base>, + ) -> Result, Error> { + // Enable `q_add` selector + self.q_add.enable(region, offset)?; + + // Copy point `p` into `x_p`, `y_p` columns + copy(region, || "x_p", self.x_p, offset, &p.x, &self.perm)?; + copy(region, || "y_p", self.y_p, offset, &p.y, &self.perm)?; + + // Copy point `q` into `x_qr`, `y_qr` columns + copy(region, || "x_q", self.x_qr, offset, &q.x, &self.perm)?; + copy(region, || "y_q", self.y_qr, offset, &q.y, &self.perm)?; + + let (x_p, y_p) = (p.x.value(), p.y.value()); + let (x_q, y_q) = (q.x.value(), q.y.value()); + + // Assign α = inv0(x_q - x_p) + let alpha = x_p.zip(x_q).map(|(x_p, x_q)| inv0(x_q - x_p)); + region.assign_advice( + || "α", + self.alpha, + offset, + || alpha.ok_or(Error::SynthesisError), + )?; + + // Assign β = inv0(x_p) + region.assign_advice( + || "β", + self.beta, + offset, + || { + let beta = x_p.map(inv0); + beta.ok_or(Error::SynthesisError) + }, + )?; + + // Assign γ = inv0(x_q) + region.assign_advice( + || "γ", + self.gamma, + offset, + || { + let gamma = x_q.map(inv0); + gamma.ok_or(Error::SynthesisError) + }, + )?; + + // Assign δ = inv0(y_q + y_p) if x_q = x_p, 0 otherwise + region.assign_advice( + || "δ", + self.delta, + offset, + || { + let x_p = x_p.ok_or(Error::SynthesisError)?; + let x_q = x_q.ok_or(Error::SynthesisError)?; + let y_p = y_p.ok_or(Error::SynthesisError)?; + let y_q = y_q.ok_or(Error::SynthesisError)?; + + let delta = if x_q == x_p { + inv0(y_q + y_p) + } else { + C::Base::zero() + }; + Ok(delta) + }, + )?; + + #[allow(clippy::collapsible_else_if)] + // Assign lambda + let lambda = x_p + .zip(y_p) + .zip(x_q) + .zip(y_q) + .map(|(((x_p, y_p), x_q), y_q)| { + if x_q != x_p { + // λ = (y_q - y_p)/(x_q - x_p) + // Here, alpha = inv0(x_q - x_p), which suffices since we + // know that x_q != x_p in this branch. + (y_q - y_p) * alpha.unwrap() + } else { + if y_p != C::Base::zero() { + // 3(x_p)^2 + let three_x_p_sq = C::Base::from_u64(3) * x_p * x_p; + // 2(y_p) + let two_y_p = C::Base::from_u64(2) * y_p; + // λ = 3(x_p)^2 / 2(y_p) + three_x_p_sq * two_y_p.invert().unwrap() + } else { + C::Base::zero() + } + } + }); + region.assign_advice( + || "λ", + self.lambda, + offset, + || lambda.ok_or(Error::SynthesisError), + )?; + + // Assign x_r + let x_r = + x_p.zip(y_p) + .zip(x_q) + .zip(y_q) + .zip(lambda) + .map(|((((x_p, y_p), x_q), y_q), lambda)| { + if x_p == C::Base::zero() { + // 0 + Q = Q + x_q + } else if x_q == C::Base::zero() { + // P + 0 = P + x_p + } else if (x_q == x_p) && (y_q == -y_p) { + // P + (-P) maps to (0,0) + C::Base::zero() + } else { + // x_r = λ^2 - x_p - x_q + lambda * lambda - x_p - x_q + } + }); + let x_r_cell = region.assign_advice( + || "x_r", + self.x_qr, + offset + 1, + || x_r.ok_or(Error::SynthesisError), + )?; + + // Assign y_r + let y_r = x_p.zip(y_p).zip(x_q).zip(y_q).zip(x_r).zip(lambda).map( + |(((((x_p, y_p), x_q), y_q), x_r), lambda)| { + if x_p == C::Base::zero() { + // 0 + Q = Q + y_q + } else if x_q == C::Base::zero() { + // P + 0 = P + y_p + } else if (x_q == x_p) && (y_q == -y_p) { + // P + (-P) maps to (0,0) + C::Base::zero() + } else { + // y_r = λ(x_p - x_r) - y_p + lambda * (x_p - x_r) - y_p + } + }, + ); + let y_r_cell = region.assign_advice( + || "y_r", + self.y_qr, + offset + 1, + || y_r.ok_or(Error::SynthesisError), + )?; + + let result = EccPoint:: { + x: CellValue::::new(x_r_cell, x_r), + y: CellValue::::new(y_r_cell, y_r), + }; + + #[cfg(test)] + // Check that the correct sum is obtained. + { + use group::Curve; + + let p = p.point(); + let q = q.point(); + let real_sum = p.zip(q).map(|(p, q)| p + q); + let result = result.point(); + + if let (Some(real_sum), Some(result)) = (real_sum, result) { + assert_eq!(real_sum.to_affine(), result); + } + } + + Ok(result) + } +} + +// inv0(x) is 0 if x = 0, 1/x otherwise. +fn inv0(x: F) -> F { + if x == F::zero() { + F::zero() + } else { + x.invert().unwrap() + } +} + +#[cfg(test)] +pub mod tests { + use group::Curve; + use halo2::{ + arithmetic::{CurveAffine, CurveExt}, + circuit::Layouter, + plonk::Error, + }; + + use crate::circuit::gadget::ecc::{EccInstructions, Point}; + + #[allow(clippy::too_many_arguments)] + pub fn test_add + Clone + Eq + std::fmt::Debug>( + chip: EccChip, + mut layouter: impl Layouter, + zero: &Point, + p_val: C, + p: &Point, + q_val: C, + q: &Point, + p_neg: &Point, + ) -> Result<(), Error> { + // Make sure P and Q are not the same point. + assert_ne!(p_val, q_val); + + // Check complete addition P + (-P) + p.add(layouter.namespace(|| "P + (-P)"), &p_neg)?; + + // Check complete addition 𝒪 + 𝒪 + zero.add(layouter.namespace(|| "𝒪 + 𝒪"), &zero)?; + + // Check P + Q + p.add(layouter.namespace(|| "P + Q"), &q)?; + + // P + P + p.add(layouter.namespace(|| "P + P"), &p)?; + + // P + 𝒪 + p.add(layouter.namespace(|| "P + 𝒪"), &zero)?; + + // 𝒪 + P + zero.add(layouter.namespace(|| "𝒪 + P"), &p)?; + + // (x, y) + (ζx, y) should behave like normal P + Q. + let endo_p = p_val.to_curve().endo(); + let endo_p = Point::new( + chip.clone(), + layouter.namespace(|| "point"), + Some(endo_p.to_affine()), + )?; + p.add(layouter.namespace(|| "P + endo(P)"), &endo_p)?; + + // (x, y) + (ζx, -y) should also behave like normal P + Q. + let endo_p_neg = (-p_val).to_curve().endo(); + let endo_p_neg = Point::new( + chip.clone(), + layouter.namespace(|| "point"), + Some(endo_p_neg.to_affine()), + )?; + p.add(layouter.namespace(|| "P + endo(-P)"), &endo_p_neg)?; + + // (x, y) + ((ζ^2)x, y) + let endo_2_p = p_val.to_curve().endo().endo(); + let endo_2_p = Point::new( + chip.clone(), + layouter.namespace(|| "point"), + Some(endo_2_p.to_affine()), + )?; + p.add(layouter.namespace(|| "P + endo(P)"), &endo_2_p)?; + + // (x, y) + ((ζ^2)x, -y) + let endo_2_p_neg = (-p_val).to_curve().endo().endo(); + let endo_2_p_neg = Point::new( + chip, + layouter.namespace(|| "point"), + Some(endo_2_p_neg.to_affine()), + )?; + p.add(layouter.namespace(|| "P + endo(P)"), &endo_2_p_neg)?; + + Ok(()) + } +} diff --git a/src/circuit/gadget/ecc/chip/add_incomplete.rs b/src/circuit/gadget/ecc/chip/add_incomplete.rs new file mode 100644 index 000000000..4205ef997 --- /dev/null +++ b/src/circuit/gadget/ecc/chip/add_incomplete.rs @@ -0,0 +1,200 @@ +use super::{copy, CellValue, EccConfig, EccPoint, Var}; +use ff::Field; +use group::Curve; +use halo2::{ + arithmetic::{CurveAffine, FieldExt}, + circuit::Region, + plonk::{Advice, Column, ConstraintSystem, Error, Permutation, Selector}, + poly::Rotation, +}; + +#[derive(Clone, Debug)] +pub struct Config { + q_add_incomplete: Selector, + // x-coordinate of P in P + Q = R + pub x_p: Column, + // y-coordinate of P in P + Q = R + pub y_p: Column, + // x-coordinate of Q or R in P + Q = R + pub x_qr: Column, + // y-coordinate of Q or R in P + Q = R + pub y_qr: Column, + // Permutation + perm: Permutation, +} + +impl From<&EccConfig> for Config { + fn from(ecc_config: &EccConfig) -> Self { + Self { + q_add_incomplete: ecc_config.q_add_incomplete, + x_p: ecc_config.advices[0], + y_p: ecc_config.advices[1], + x_qr: ecc_config.advices[2], + y_qr: ecc_config.advices[3], + perm: ecc_config.perm.clone(), + } + } +} + +impl Config { + pub(super) fn create_gate(&self, meta: &mut ConstraintSystem) { + meta.create_gate("incomplete addition gates", |meta| { + let q_add_incomplete = meta.query_selector(self.q_add_incomplete, Rotation::cur()); + let x_p = meta.query_advice(self.x_p, Rotation::cur()); + let y_p = meta.query_advice(self.y_p, Rotation::cur()); + let x_q = meta.query_advice(self.x_qr, Rotation::cur()); + let y_q = meta.query_advice(self.y_qr, Rotation::cur()); + let x_r = meta.query_advice(self.x_qr, Rotation::next()); + let y_r = meta.query_advice(self.y_qr, Rotation::next()); + + // (x_r + x_q + x_p)⋅(x_p − x_q)^2 − (y_p − y_q)^2 = 0 + let poly1 = { + (x_r.clone() + x_q.clone() + x_p.clone()) + * (x_p.clone() - x_q.clone()) + * (x_p.clone() - x_q.clone()) + - (y_p.clone() - y_q.clone()) * (y_p.clone() - y_q.clone()) + }; + + // (y_r + y_q)(x_p − x_q) − (y_p − y_q)(x_q − x_r) = 0 + let poly2 = (y_r + y_q.clone()) * (x_p - x_q.clone()) - (y_p - y_q) * (x_q - x_r); + + [poly1, poly2] + .iter() + .map(|poly| q_add_incomplete.clone() * poly.clone()) + .collect() + }); + } + + pub(super) fn assign_region( + &self, + p: &EccPoint, + q: &EccPoint, + offset: usize, + region: &mut Region<'_, C::Base>, + ) -> Result, Error> { + // Enable `q_add_incomplete` selector + self.q_add_incomplete.enable(region, offset)?; + + // Handle exceptional cases + let (x_p, y_p) = (p.x.value(), p.y.value()); + let (x_q, y_q) = (q.x.value(), q.y.value()); + x_p.zip(y_p) + .zip(x_q) + .zip(y_q) + .map(|(((x_p, y_p), x_q), y_q)| { + // P is point at infinity + if (x_p == C::Base::zero() && y_p == C::Base::zero()) + // Q is point at infinity + || (x_q == C::Base::zero() && y_q == C::Base::zero()) + // x_p = x_q + || (x_p == x_q) + { + Err(Error::SynthesisError) + } else { + Ok(()) + } + }) + .transpose()?; + + // Copy point `p` into `x_p`, `y_p` columns + copy(region, || "x_p", self.x_p, offset, &p.x, &self.perm)?; + copy(region, || "y_p", self.y_p, offset, &p.y, &self.perm)?; + + // Copy point `q` into `x_qr`, `y_qr` columns + copy(region, || "x_q", self.x_qr, offset, &q.x, &self.perm)?; + copy(region, || "y_q", self.y_qr, offset, &q.y, &self.perm)?; + + // Compute the sum `P + Q = R` + let r = { + let p = p.point(); + let q = q.point(); + let r = p + .zip(q) + .map(|(p, q)| (p + q).to_affine().coordinates().unwrap()); + let r_x = r.map(|r| *r.x()); + let r_y = r.map(|r| *r.y()); + + (r_x, r_y) + }; + + // Assign the sum to `x_qr`, `y_qr` columns in the next row + let x_r = r.0; + let x_r_var = region.assign_advice( + || "x_r", + self.x_qr, + offset + 1, + || x_r.ok_or(Error::SynthesisError), + )?; + + let y_r = r.1; + let y_r_var = region.assign_advice( + || "y_r", + self.y_qr, + offset + 1, + || y_r.ok_or(Error::SynthesisError), + )?; + + let result = EccPoint:: { + x: CellValue::::new(x_r_var, x_r), + y: CellValue::::new(y_r_var, y_r), + }; + + #[cfg(test)] + // Check that the correct sum is obtained. + { + let p = p.point(); + let q = q.point(); + let real_sum = p.zip(q).map(|(p, q)| p + q); + let result = result.point(); + + if let (Some(real_sum), Some(result)) = (real_sum, result) { + assert_eq!(real_sum.to_affine(), result); + } + } + + Ok(result) + } +} + +#[cfg(test)] +pub mod tests { + use halo2::{arithmetic::CurveAffine, circuit::Layouter, plonk::Error}; + + use crate::circuit::gadget::ecc::{EccInstructions, Point}; + + pub fn test_add_incomplete< + C: CurveAffine, + EccChip: EccInstructions + Clone + Eq + std::fmt::Debug, + >( + mut layouter: impl Layouter, + zero: &Point, + p: &Point, + q: &Point, + p_neg: &Point, + ) -> Result<(), Error> { + // P + Q + p.add_incomplete(layouter.namespace(|| "P + Q"), &q)?; + + // P + P should return an error + p.add_incomplete(layouter.namespace(|| "P + P"), &p) + .expect_err("P + P should return an error"); + + // P + (-P) should return an error + p.add_incomplete(layouter.namespace(|| "P + (-P)"), &p_neg) + .expect_err("P + (-P) should return an error"); + + // P + 𝒪 should return an error + p.add_incomplete(layouter.namespace(|| "P + 𝒪"), &zero) + .expect_err("P + 0 should return an error"); + + // 𝒪 + P should return an error + zero.add_incomplete(layouter.namespace(|| "𝒪 + P"), &p) + .expect_err("0 + P should return an error"); + + // 𝒪 + 𝒪 should return an error + zero.add_incomplete(layouter.namespace(|| "𝒪 + 𝒪"), &zero) + .expect_err("𝒪 + 𝒪 should return an error"); + + Ok(()) + } +} diff --git a/src/circuit/gadget/ecc/chip/mul.rs b/src/circuit/gadget/ecc/chip/mul.rs new file mode 100644 index 000000000..d6e59b9a2 --- /dev/null +++ b/src/circuit/gadget/ecc/chip/mul.rs @@ -0,0 +1,453 @@ +use super::{add, copy, CellValue, EccConfig, EccPoint, Var}; +use crate::constants::NUM_COMPLETE_BITS; +use std::ops::{Deref, Range}; + +use ff::PrimeField; +use halo2::{ + arithmetic::{CurveAffine, Field, FieldExt}, + circuit::Region, + plonk::{Advice, Column, ConstraintSystem, Error, Expression, Permutation, Selector}, + poly::Rotation, +}; + +mod complete; +mod incomplete; + +pub struct Config { + // Selector used to constrain the cells used in complete addition. + q_mul_complete: Selector, + // Selector used to check recovery of the original scalar after decomposition. + q_mul_decompose_var: Selector, + // Advice column used to decompose scalar in complete addition. + z_complete: Column, + // Advice column where the scalar is copied for use in the final recovery check. + scalar: Column, + // Permutation + perm: Permutation, + // Configuration used in complete addition + add_config: add::Config, + // Configuration used for `hi` bits of the scalar + hi_config: incomplete::Config, + // Configuration used for `lo` bits of the scalar + lo_config: incomplete::Config, +} + +impl From<&EccConfig> for Config { + fn from(ecc_config: &EccConfig) -> Self { + let config = Self { + q_mul_complete: ecc_config.q_mul_complete, + q_mul_decompose_var: ecc_config.q_mul_decompose_var, + z_complete: ecc_config.advices[9], + scalar: ecc_config.advices[1], + perm: ecc_config.perm.clone(), + add_config: ecc_config.into(), + hi_config: incomplete::Config::hi_config(ecc_config), + lo_config: incomplete::Config::lo_config(ecc_config), + }; + + assert_eq!( + config.hi_config.x_p, config.lo_config.x_p, + "x_p is shared across hi and lo halves." + ); + assert_eq!( + config.hi_config.y_p, config.lo_config.y_p, + "y_p is shared across hi and lo halves." + ); + + let add_config_advices = config.add_config.advice_columns(); + assert!( + !add_config_advices.contains(&config.z_complete), + "z_complete cannot overlap with complete addition columns." + ); + assert!( + !add_config_advices.contains(&config.hi_config.z), + "hi_config z cannot overlap with complete addition columns." + ); + + config + } +} + +impl Config { + pub(super) fn create_gate(&self, meta: &mut ConstraintSystem) { + self.hi_config.create_gate(meta); + self.lo_config.create_gate(meta); + + let complete_config: complete::Config = self.into(); + complete_config.create_gate(meta); + + self.create_final_scalar_gate(meta); + } + + /// Gate used to check final scalar is recovered. + fn create_final_scalar_gate(&self, meta: &mut ConstraintSystem) { + meta.create_gate("Decompose scalar for variable-base mul", |meta| { + let q_mul_decompose_var = + meta.query_selector(self.q_mul_decompose_var, Rotation::cur()); + let scalar = meta.query_advice(self.scalar, Rotation::cur()); + let z_cur = meta.query_advice(self.z_complete, Rotation::cur()); + + // The scalar field `F_q = 2^254 + t_q`. + // -((2^127)^2) = -(2^254) = t_q (mod q) + let t_q = -(C::Scalar::from_u128(1u128 << 127).square()); + let t_q = C::Base::from_bytes(&t_q.to_bytes()).unwrap(); + + // Check that `k = scalar + t_q` + vec![q_mul_decompose_var * (scalar + Expression::Constant(t_q) - z_cur)] + }); + } + + pub(super) fn assign_region( + &self, + scalar: &CellValue, + base: &EccPoint, + offset: usize, + region: &mut Region<'_, C::Base>, + ) -> Result, Error> { + // Initialize the accumulator `acc = [2]base` + let acc = self + .add_config + .assign_region(&base, &base, offset, region)?; + + // Increase the offset by 1 after complete addition. + let offset = offset + 1; + + // Decompose the scalar bitwise (big-endian bit order). + let bits = decompose_for_scalar_mul::(scalar.value()); + + // Initialize the running sum for scalar decomposition to zero + let z_val = C::Base::zero(); + let z_cell = + region.assign_advice(|| "initial z", self.hi_config.z, offset, || Ok(z_val))?; + let z = CellValue::new(z_cell, Some(z_val)); + + // Increase the offset by 1 after initializing `z`. + let offset = offset + 1; + + // Double-and-add (incomplete addition) for the `hi` half of the scalar decomposition + let bits_incomplete_hi = &bits[incomplete_hi_range::()]; + let (x, y_a, z) = self.hi_config.double_and_add( + region, + offset, + &base, + bits_incomplete_hi, + (X(acc.x), Y(acc.y.value()), Z(z)), + )?; + + // Double-and-add (incomplete addition) for the `lo` half of the scalar decomposition + let bits_incomplete_lo = &bits[incomplete_lo_range::()]; + let (x, y_a, z) = self.lo_config.double_and_add( + region, + offset, + &base, + bits_incomplete_lo, + (x, y_a, z), + )?; + + // Move from incomplete addition to complete addition + let offset = offset + incomplete_lo_len::() + 1; + + // Get value of acc after incomplete addition + let acc = { + // Assign final `y_a` output from incomplete addition + let y_a_cell = region.assign_advice( + || "y_a", + self.add_config.y_qr, + offset, + || y_a.ok_or(Error::SynthesisError), + )?; + + EccPoint { + x: x.0, + y: CellValue::::new(y_a_cell, *y_a), + } + }; + + // Initialize `z` running sum for complete addition + copy( + region, + || "Initialize `z` running sum for complete addition", + self.z_complete, + offset, + &z, + &self.perm, + )?; + + // Increase the offset by 1 after complete addition. + let offset = offset + 1; + + // Complete addition + let (acc, z_val) = { + let complete_config: complete::Config = self.into(); + // Bits used in complete addition. k_{3} to k_{1} inclusive + // The LSB k_{0} is handled separately. + let bits_complete = &bits[complete_range::()]; + complete_config.assign_region(region, offset, bits_complete, base, acc, z.value())? + }; + + let offset = offset + complete_len::() * 2; + + // Process the least significant bit + let lsb = bits[C::Scalar::NUM_BITS as usize - 1]; + let result = self.process_lsb(region, offset, scalar, base, acc, lsb, z_val)?; + + #[cfg(test)] + // Check that the correct multiple is obtained. + { + use group::Curve; + + let base = base.point(); + let scalar = scalar + .value() + .map(|scalar| C::Scalar::from_bytes(&scalar.to_bytes()).unwrap()); + let real_mul = base.zip(scalar).map(|(base, scalar)| base * scalar); + let result = result.point(); + + if let (Some(real_mul), Some(result)) = (real_mul, result) { + assert_eq!(real_mul.to_affine(), result); + } + } + + Ok(result) + } + + #[allow(clippy::too_many_arguments)] + fn process_lsb( + &self, + region: &mut Region<'_, C::Base>, + offset: usize, + scalar: &CellValue, + base: &EccPoint, + acc: EccPoint, + lsb: Option, + mut z_val: Option, + ) -> Result, Error> { + // Assign the final `z` value. + z_val = z_val + .zip(lsb) + .map(|(z_val, lsb)| C::Base::from_u64(2) * z_val + C::Base::from_u64(lsb as u64)); + region.assign_advice( + || "final z", + self.z_complete, + offset, + || z_val.ok_or(Error::SynthesisError), + )?; + + // Check that we recover the original scalar. + // + // NB: We assume that the scalar fits in the curve's base field. This is not + // true in general, and in particular for the Pallas curve, whose scalar field + // `Fq` is larger than its base field `Fp`. + // + // However, the only use of variable-base scalar mul in the Orchard protocol + // is in deriving diversified addresses `[ivk] g_d`, and `ivk` is guaranteed + // to be in the base field of the curve. (See non-normative notes in + // https://zips.z.cash/protocol/nu5.pdf#orchardkeycomponents.) + copy( + region, + || "original scalar", + self.scalar, + offset, + &scalar, + &self.perm, + )?; + self.q_mul_decompose_var.enable(region, offset)?; + + // If `lsb` is 0, return `Acc + (-P)`. If `lsb` is 1, simply return `Acc + 0`. + let x_p = if let Some(lsb) = lsb { + if !lsb { + base.x.value() + } else { + Some(C::Base::zero()) + } + } else { + None + }; + let y_p = if let Some(lsb) = lsb { + if !lsb { + base.y.value().map(|y_p| -y_p) + } else { + Some(C::Base::zero()) + } + } else { + None + }; + + let x_p_cell = region.assign_advice( + || "x_p", + self.add_config.x_p, + offset + 1, + || x_p.ok_or(Error::SynthesisError), + )?; + + let y_p_cell = region.assign_advice( + || "y_p", + self.add_config.y_p, + offset + 1, + || y_p.ok_or(Error::SynthesisError), + )?; + + let p = EccPoint { + x: CellValue::::new(x_p_cell, x_p), + y: CellValue::::new(y_p_cell, y_p), + }; + + // Return the result of the final complete addition as `[scalar]B` + self.add_config.assign_region(&p, &acc, offset + 1, region) + } +} + +#[derive(Clone, Debug)] +// `x`-coordinate of the accumulator. +struct X(CellValue); +impl Deref for X { + type Target = CellValue; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Copy, Clone, Debug)] +// `y`-coordinate of the accumulator. +struct Y(Option); +impl Deref for Y { + type Target = Option; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Clone, Debug)] +// Cumulative sum `z` used to decompose the scalar. +struct Z(CellValue); +impl Deref for Z { + type Target = CellValue; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +fn decompose_for_scalar_mul(scalar: Option) -> Vec> { + let bits = scalar.map(|scalar| { + // Cast from base field into scalar field. + // Assumptions: + // - The witnessed scalar field element fits into the base field. + // - The scalar field order is larger than the base field order. + let scalar = C::Scalar::from_bytes(&scalar.to_bytes()).unwrap(); + + // The scalar field `F_q = 2^254 + t_q`. + // -((2^127)^2) = -(2^254) = t_q (mod q) + // + // Assumptions: + // - The scalar field can be represented in 255 bits. + assert_eq!(C::Scalar::NUM_BITS, 255); + let t_q = -(C::Scalar::from_u128(1u128 << 127).square()); + + // We will witness `k = scalar + t_q` + // `k` is decomposed bitwise in-circuit for our double-and-add algorithm. + let k = scalar + t_q; + + // `k` is decomposed bitwise (big-endian) into `[k_n, ..., lsb]`, where + // each `k_i` is a bit and `scalar = k_n * 2^n + ... + k_1 * 2 + lsb`. + let mut bits: Vec = k + .to_le_bits() + .into_iter() + .take(C::Scalar::NUM_BITS as usize) + .collect(); + bits.reverse(); + assert_eq!(bits.len(), C::Scalar::NUM_BITS as usize); + + bits + }); + + if let Some(bits) = bits { + bits.into_iter().map(Some).collect() + } else { + vec![None; C::Scalar::NUM_BITS as usize] + } +} + +// Bits used in incomplete addition. k_{254} to k_{4} inclusive +fn incomplete_len() -> usize { + C::Scalar::NUM_BITS as usize - 1 - NUM_COMPLETE_BITS +} + +fn incomplete_range() -> Range { + 0..incomplete_len::() +} + +// Bits used in `lo` half of incomplete addition +fn incomplete_lo_range() -> Range { + (incomplete_len::() / 2)..incomplete_len::() +} + +// Bits used in `hi` half of incomplete addition +fn incomplete_hi_range() -> Range { + 0..(incomplete_len::() / 2) +} + +// Bits k_{254} to k_{4} inclusive are used in incomplete addition. +// The `lo` half is k_{129} to k_{4} inclusive (length 126 bits). +fn incomplete_lo_len() -> usize { + (incomplete_len::() + 1) / 2 +} + +// Bits k_{254} to k_{4} inclusive are used in incomplete addition. +// The `hi` half is k_{254} to k_{130} inclusive (length 125 bits). +fn incomplete_hi_len() -> usize { + incomplete_len::() / 2 +} + +fn complete_range() -> Range { + incomplete_len::()..(C::Scalar::NUM_BITS as usize - 1) +} + +fn complete_len() -> usize { + NUM_COMPLETE_BITS as usize +} + +#[cfg(test)] +pub mod tests { + use ff::Field; + use halo2::{ + arithmetic::{CurveAffine, FieldExt}, + circuit::Layouter, + plonk::Error, + }; + + use crate::circuit::gadget::ecc::{EccInstructions, Point, ScalarVar}; + + pub fn test_mul + Clone + Eq + std::fmt::Debug>( + chip: EccChip, + mut layouter: impl Layouter, + zero: &Point, + p: &Point, + ) -> Result<(), Error> { + let scalar_val = C::Scalar::rand(); + let scalar_val = C::Base::from_bytes(&scalar_val.to_bytes()).unwrap(); + let scalar = ScalarVar::new( + chip.clone(), + layouter.namespace(|| "ScalarVar"), + Some(scalar_val), + )?; + + // [a]B + p.mul(layouter.namespace(|| "mul"), &scalar)?; + + // [a]𝒪 should return an error since variable-base scalar multiplication + // uses incomplete addition at the beginning of its double-and-add. + zero.mul(layouter.namespace(|| "mul"), &scalar) + .expect_err("[a]𝒪 should return an error"); + + // [0]B should return (0,0) since variable-base scalar multiplication + // uses complete addition for the final bits of the scalar. + let scalar_val = C::Base::zero(); + let scalar = ScalarVar::new(chip, layouter.namespace(|| "ScalarVar"), Some(scalar_val))?; + p.mul(layouter.namespace(|| "mul"), &scalar)?; + + Ok(()) + } +} diff --git a/src/circuit/gadget/ecc/chip/mul/complete.rs b/src/circuit/gadget/ecc/chip/mul/complete.rs new file mode 100644 index 000000000..c14a41aa2 --- /dev/null +++ b/src/circuit/gadget/ecc/chip/mul/complete.rs @@ -0,0 +1,163 @@ +use super::super::{add, copy, CellValue, EccPoint, Var}; +use super::complete_len; +use ff::Field; + +use halo2::{ + arithmetic::{CurveAffine, FieldExt}, + circuit::Region, + plonk::{Advice, Column, ConstraintSystem, Error, Expression, Permutation, Selector}, + poly::Rotation, +}; +use std::marker::PhantomData; + +pub struct Config { + // Selector used to constrain the cells used in complete addition. + q_mul_complete: Selector, + // Advice column used to decompose scalar in complete addition. + z_complete: Column, + // Permutation + perm: Permutation, + // Configuration used in complete addition + add_config: add::Config, + _marker: PhantomData, +} + +impl From<&super::Config> for Config { + fn from(config: &super::Config) -> Self { + Self { + q_mul_complete: config.q_mul_complete, + z_complete: config.z_complete, + perm: config.perm.clone(), + add_config: config.add_config.clone(), + _marker: PhantomData, + } + } +} + +impl Config { + /// Gate used to check scalar decomposition is correct. + /// This is used to check the bits used in complete addition, since the incomplete + /// addition gate (controlled by `q_mul`) already checks scalar decomposition for + /// the other bits. + pub(super) fn create_gate(&self, meta: &mut ConstraintSystem) { + meta.create_gate( + "Decompose scalar for complete bits of variable-base mul", + |meta| { + let q_mul_complete = meta.query_selector(self.q_mul_complete, Rotation::cur()); + let z_cur = meta.query_advice(self.z_complete, Rotation::cur()); + let z_prev = meta.query_advice(self.z_complete, Rotation::prev()); + + // k_{i} = z_{i} - 2⋅z_{i+1} + let k = z_cur - Expression::Constant(C::Base::from_u64(2)) * z_prev; + // (k_i) ⋅ (k_i - 1) = 0 + let bool_check = k.clone() * (k + Expression::Constant(-C::Base::one())); + + vec![q_mul_complete * bool_check] + }, + ); + } + + #[allow(clippy::type_complexity)] + pub(super) fn assign_region( + &self, + region: &mut Region<'_, C::Base>, + offset: usize, + bits: &[Option], + base: &EccPoint, + mut acc: EccPoint, + mut z_val: Option, + ) -> Result<(EccPoint, Option), Error> { + // Make sure we have the correct number of bits for the complete addition + // part of variable-base scalar mul. + assert_eq!(bits.len(), complete_len::()); + + // Enable selectors for complete range + for row in 0..complete_len::() { + // Each iteration uses 2 rows (two complete additions) + let row = 2 * row; + + self.q_mul_complete.enable(region, row + offset)?; + } + + // Complete addition + for (iter, k) in bits.iter().enumerate() { + // Each iteration uses 2 rows (two complete additions) + let row = 2 * iter; + + // Check scalar decomposition here + region.assign_advice( + || "z", + self.z_complete, + row + offset - 1, + || z_val.ok_or(Error::SynthesisError), + )?; + z_val = z_val + .zip(k.as_ref()) + .map(|(z_val, k)| C::Base::from_u64(2) * z_val + C::Base::from_u64(*k as u64)); + region.assign_advice( + || "z", + self.z_complete, + row + offset, + || z_val.ok_or(Error::SynthesisError), + )?; + + // Assign `x_p` for complete addition + let x_p = base.x.value(); + let x_p_cell = region.assign_advice( + || "x_p", + self.add_config.x_p, + row + offset, + || x_p.ok_or(Error::SynthesisError), + )?; + + // Assign `y_p` for complete addition. + // If the bit is set, use `y`; if the bit is not set, use `-y` + let y_p = base.y.value(); + let y_p = y_p + .zip(k.as_ref()) + .map(|(y_p, k)| if !k { -y_p } else { y_p }); + + let y_p_cell = region.assign_advice( + || "y_p", + self.add_config.y_p, + row + offset, + || y_p.ok_or(Error::SynthesisError), + )?; + let p = EccPoint { + x: CellValue::::new(x_p_cell, x_p), + y: CellValue::::new(y_p_cell, y_p), + }; + + // Acc + U + let tmp_acc = self + .add_config + .assign_region(&p, &acc, row + offset, region)?; + + // Copy acc from `x_a`, `y_a` over to `x_p`, `y_p` on the next row + let acc_x = copy( + region, + || "copy acc x_a", + self.add_config.x_p, + row + offset + 1, + &acc.x, + &self.perm, + )?; + let acc_y = copy( + region, + || "copy acc y_a", + self.add_config.y_p, + row + offset + 1, + &acc.y, + &self.perm, + )?; + + acc = EccPoint { x: acc_x, y: acc_y }; + + // Acc + P + Acc + acc = self + .add_config + .assign_region(&acc, &tmp_acc, row + offset + 1, region)?; + } + Ok((acc, z_val)) + } +} diff --git a/src/circuit/gadget/ecc/chip/mul/incomplete.rs b/src/circuit/gadget/ecc/chip/mul/incomplete.rs new file mode 100644 index 000000000..0aed00e28 --- /dev/null +++ b/src/circuit/gadget/ecc/chip/mul/incomplete.rs @@ -0,0 +1,300 @@ +use super::super::{copy, CellValue, EccConfig, EccPoint, Var}; +use super::{incomplete_hi_len, incomplete_lo_len, X, Y, Z}; +use ff::Field; +use halo2::{ + arithmetic::{CurveAffine, FieldExt}, + circuit::Region, + plonk::{Advice, Column, ConstraintSystem, Error, Expression, Permutation, Selector}, + poly::Rotation, +}; +use std::marker::PhantomData; + +pub(super) struct Config { + // Number of bits covered by this incomplete range. + num_bits: usize, + // Selector used to constrain the cells used in incomplete addition. + pub(super) q_mul: Selector, + // Cumulative sum used to decompose the scalar. + pub(super) z: Column, + // x-coordinate of the accumulator in each double-and-add iteration. + pub(super) x_a: Column, + // x-coordinate of the point being added in each double-and-add iteration. + pub(super) x_p: Column, + // y-coordinate of the point being added in each double-and-add iteration. + pub(super) y_p: Column, + // lambda1 in each double-and-add iteration. + pub(super) lambda1: Column, + // lambda2 in each double-and-add iteration. + pub(super) lambda2: Column, + // Permutation + pub(super) perm: Permutation, + _marker: PhantomData, +} + +impl Config { + // Columns used in processing the `hi` bits of the scalar. + // `x_p, y_p` are shared across the `hi` and `lo` halves. + pub(super) fn hi_config(ecc_config: &EccConfig) -> Self { + Self { + num_bits: incomplete_hi_len::(), + q_mul: ecc_config.q_mul_hi, + x_p: ecc_config.advices[0], + y_p: ecc_config.advices[1], + z: ecc_config.advices[9], + x_a: ecc_config.advices[3], + lambda1: ecc_config.advices[4], + lambda2: ecc_config.advices[5], + perm: ecc_config.perm.clone(), + _marker: PhantomData, + } + } + + // Columns used in processing the `lo` bits of the scalar. + // `x_p, y_p` are shared across the `hi` and `lo` halves. + pub(super) fn lo_config(ecc_config: &EccConfig) -> Self { + Self { + num_bits: incomplete_lo_len::(), + q_mul: ecc_config.q_mul_lo, + x_p: ecc_config.advices[0], + y_p: ecc_config.advices[1], + z: ecc_config.advices[6], + x_a: ecc_config.advices[7], + lambda1: ecc_config.advices[8], + lambda2: ecc_config.advices[2], + perm: ecc_config.perm.clone(), + _marker: PhantomData, + } + } + + // Gate for incomplete addition part of variable-base scalar multiplication. + pub(super) fn create_gate(&self, meta: &mut ConstraintSystem) { + // (k_i) ⋅ (k_i - 1) = 0 + meta.create_gate("Scalar boolean decomposition", |meta| { + let q_mul = meta.query_selector(self.q_mul, Rotation::cur()); + let z_cur = meta.query_advice(self.z, Rotation::cur()); + let z_prev = meta.query_advice(self.z, Rotation::prev()); + let x_a_cur = meta.query_advice(self.x_a, Rotation::cur()); + let x_a_next = meta.query_advice(self.x_a, Rotation::next()); + let x_p_cur = meta.query_advice(self.x_p, Rotation::cur()); + let x_p_next = meta.query_advice(self.x_p, Rotation::next()); + let y_p_cur = meta.query_advice(self.y_p, Rotation::cur()); + let y_p_next = meta.query_advice(self.y_p, Rotation::next()); + let lambda1_cur = meta.query_advice(self.lambda1, Rotation::cur()); + let lambda2_cur = meta.query_advice(self.lambda2, Rotation::cur()); + let lambda1_next = meta.query_advice(self.lambda1, Rotation::next()); + let lambda2_next = meta.query_advice(self.lambda2, Rotation::next()); + + // The current bit in the scalar decomposition, k_i = z_i - 2⋅z_{i+1}. + // Recall that we assigned the cumulative variable `z_i` in descending order, + // i from n down to 0. So z_{i+1} corresponds to the `z_prev` query. + let k = z_cur - Expression::Constant(F::from_u64(2)) * z_prev; + + // y_{A,i} = (λ_{1,i} + λ_{2,i}) + // * (x_{A,i} - (λ_{1,i}^2 - x_{A,i} - x_{P,i})) / 2 + let y_a_cur = (lambda1_cur.clone() + lambda2_cur.clone()) + * (x_a_cur.clone() + - (lambda1_cur.clone() * lambda1_cur.clone() + - x_a_cur.clone() + - x_p_cur.clone())) + * F::TWO_INV; + + // y_{A,i+1} = (λ_{1,i+1} + λ_{2,i+1}) + // * (x_{A,i+1} - (λ_{1,i+1}^2 - x_{A,i+1} - x_{P,i+1})) / 2 + let y_a_next = (lambda1_next.clone() + lambda2_next) + * (x_a_next.clone() + - (lambda1_next.clone() * lambda1_next - x_a_next.clone() - x_p_next.clone())) + * F::TWO_INV; + + // Check booleanity of decomposition. + let bool_check = k.clone() * (k.clone() + Expression::Constant(-F::one())); + + // The base used in double-and-add remains constant. We check that its + // x- and y- coordinates are the same throughout. + let x_p_check = x_p_cur.clone() - x_p_next; + let y_p_check = y_p_cur.clone() - y_p_next; + + // λ_{1,i}⋅(x_{A,i} − x_{P,i}) − y_{A,i} + (2k_i - 1) y_{P,i} = 0 + let poly1 = lambda1_cur.clone() * (x_a_cur.clone() - x_p_cur.clone()) - y_a_cur.clone() + + (k * F::from_u64(2) + Expression::Constant(-F::one())) * y_p_cur; + + // (λ_{1,i} + λ_{2,i})⋅(x_{A,i} − (λ_{1,i}^2 − x_{A,i} − x_{P,i})) − 2y_{A,i}) = 0 + let poly2 = { + let lambda_neg = lambda1_cur.clone() + lambda2_cur.clone(); + let lambda1_expr = + lambda1_cur.clone() * lambda1_cur.clone() - x_a_cur.clone() - x_p_cur.clone(); + lambda_neg * (x_a_cur.clone() - lambda1_expr) + - Expression::Constant(F::from_u64(2)) * y_a_cur.clone() + }; + + // λ_{2,i}^2 − x_{A,i+1} −(λ_{1,i}^2 − x_{A,i} − x_{P,i}) − x_{A,i} = 0 + let poly3 = lambda2_cur.clone() * lambda2_cur.clone() + - x_a_next.clone() + - (lambda1_cur.clone() * lambda1_cur) + + x_p_cur; + + // λ_{2,i}⋅(x_{A,i} − x_{A,i+1}) − y_{A,i} − y_{A,i+1} = 0 + let poly4 = lambda2_cur * (x_a_cur - x_a_next) - y_a_cur - y_a_next; + + [bool_check, x_p_check, y_p_check, poly1, poly2, poly3, poly4] + .iter() + .map(|poly| q_mul.clone() * poly.clone()) + .collect() + }); + } + + // We perform incomplete addition on all but the last three bits of the + // decomposed scalar. + // We split the bits in the incomplete addition range into "hi" and "lo" + // halves and process them side by side, using the same rows but with + // non-overlapping columns. + // Returns (x, y, z). + #[allow(clippy::type_complexity)] + pub(super) fn double_and_add( + &self, + region: &mut Region<'_, C::Base>, + offset: usize, + base: &EccPoint, + bits: &[Option], + acc: (X, Y, Z), + ) -> Result<(X, Y, Z), Error> { + // Check that we have the correct number of bits for this double-and-add. + assert_eq!(bits.len(), self.num_bits); + + // Handle exceptional cases + let (x_p, y_p) = (base.x.value(), base.y.value()); + let (x_a, y_a) = (acc.0.value(), acc.1 .0); + x_p.zip(y_p) + .zip(x_a) + .zip(y_a) + .map(|(((x_p, y_p), x_a), y_a)| { + // A is point at infinity + if (x_p == C::Base::zero() && y_p == C::Base::zero()) + // Q is point at infinity + || (x_a == C::Base::zero() && y_a == C::Base::zero()) + // x_p = x_a + || (x_p == x_a) + { + return Err(Error::SynthesisError); + } + Ok(()) + }) + .unwrap_or(Err(Error::SynthesisError))?; + + // Enable `q_mul` on all but the last row of the incomplete range. + for row in 1..(self.num_bits - 1) { + self.q_mul.enable(region, offset + row)?; + } + + // Initialise the running `z` sum for the scalar bits. + let mut z = copy(region, || "starting z", self.z, offset, &acc.2, &self.perm)?; + + // Increase offset by 1; we used row 0 for initializing `z`. + let offset = offset + 1; + + // Define `x_p`, `y_p` + let x_p = base.x.value(); + let y_p = base.y.value(); + + // Initialise acc + let mut x_a = copy( + region, + || "starting x_a", + self.x_a, + offset, + &acc.0, + &self.perm, + )?; + let mut y_a = *acc.1; + + // Incomplete addition + for (row, k) in bits.iter().enumerate() { + // z_{i} = 2 * z_{i+1} + k_i + let z_val = z + .value() + .zip(k.as_ref()) + .map(|(z_val, k)| C::Base::from_u64(2) * z_val + C::Base::from_u64(*k as u64)); + let z_cell = region.assign_advice( + || "z", + self.z, + row + offset, + || z_val.ok_or(Error::SynthesisError), + )?; + z = CellValue::new(z_cell, z_val); + + // Assign `x_p`, `y_p` + region.assign_advice( + || "x_p", + self.x_p, + row + offset, + || x_p.ok_or(Error::SynthesisError), + )?; + region.assign_advice( + || "y_p", + self.y_p, + row + offset, + || y_p.ok_or(Error::SynthesisError), + )?; + + // If the bit is set, use `y`; if the bit is not set, use `-y` + let y_p = y_p + .zip(k.as_ref()) + .map(|(y_p, k)| if !k { -y_p } else { y_p }); + + // Compute and assign λ1⋅(x_A − x_P) = y_A − y_P + let lambda1 = y_a + .zip(y_p) + .zip(x_a.value()) + .zip(x_p) + .map(|(((y_a, y_p), x_a), x_p)| (y_a - y_p) * (x_a - x_p).invert().unwrap()); + region.assign_advice( + || "lambda1", + self.lambda1, + row + offset, + || lambda1.ok_or(Error::SynthesisError), + )?; + + // x_R = λ1^2 - x_A - x_P + let x_r = lambda1 + .zip(x_a.value()) + .zip(x_p) + .map(|((lambda1, x_a), x_p)| lambda1 * lambda1 - x_a - x_p); + + // λ2 = (2(y_A) / (x_A - x_R)) - λ1 + let lambda2 = + lambda1 + .zip(y_a) + .zip(x_a.value()) + .zip(x_r) + .map(|(((lambda1, y_a), x_a), x_r)| { + C::Base::from_u64(2) * y_a * (x_a - x_r).invert().unwrap() - lambda1 + }); + region.assign_advice( + || "lambda2", + self.lambda2, + row + offset, + || lambda2.ok_or(Error::SynthesisError), + )?; + + // Compute and assign `x_a` for the next row + let x_a_new = lambda2 + .zip(x_a.value()) + .zip(x_r) + .map(|((lambda2, x_a), x_r)| lambda2 * lambda2 - x_a - x_r); + y_a = lambda2 + .zip(x_a.value()) + .zip(x_a_new) + .zip(y_a) + .map(|(((lambda2, x_a), x_a_new), y_a)| lambda2 * (x_a - x_a_new) - y_a); + let x_a_val = x_a_new; + let x_a_cell = region.assign_advice( + || "x_a", + self.x_a, + row + offset + 1, + || x_a_val.ok_or(Error::SynthesisError), + )?; + x_a = CellValue::new(x_a_cell, x_a_val); + } + + Ok((X(x_a), Y(y_a), Z(z))) + } +} 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 000000000..670552661 --- /dev/null +++ b/src/circuit/gadget/ecc/chip/mul_fixed.rs @@ -0,0 +1,483 @@ +use super::{ + add, add_incomplete, copy, witness_point, CellValue, EccConfig, EccPoint, EccScalarFixed, + EccScalarFixedShort, Var, +}; +use crate::constants::{ + self, + load::{OrchardFixedBase, OrchardFixedBasesFull, ValueCommitV, WindowUs}, +}; +use std::marker::PhantomData; + +use group::Curve; +use halo2::{ + arithmetic::{CurveAffine, Field, FieldExt}, + circuit::Region, + plonk::{Advice, Column, ConstraintSystem, Error, Expression, Fixed, Permutation, Selector}, + poly::Rotation, +}; + +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) -> C { + 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, + // Configuration for `witness_point` + witness_point_config: witness_point::Config, + _marker: PhantomData, +} + +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(), + witness_point_config: ecc_config.into(), + _marker: PhantomData, + }; + + // 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." + ); + } + + // Check relationships between this config and `witness_point_config`. + assert_eq!( + config.x_p, config.witness_point_config.x, + "witness_point is used internally in mul_fixed." + ); + assert_eq!( + config.y_p, config.witness_point_config.y, + "witness_point is used internally in mul_fixed." + ); + + config + } +} + +impl Config { + 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, Rotation::cur()); + 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(C::Base::one()), |acc, _| { + acc * window.clone() + }) + }) + .collect(); + + let interpolated_x = window_pow.iter().zip(self.lagrange_coeffs.iter()).fold( + Expression::Constant(C::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; + + [x_check, y_check] + .iter() + .map(|poly| q_mul_fixed.clone() * poly.clone()) + .collect() + }); + } + + #[allow(clippy::type_complexity)] + fn assign_region_inner( + &self, + region: &mut Region<'_, C::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<'_, C::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<'_, C::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<'_, C::Base>, + offset: usize, + base: OrchardFixedBases, + scalar: &ScalarFixed, + ) -> Result, Error> { + // Witness `m0` in `x_p`, `y_p` cells on row 0 + let k0 = scalar.windows_field()[0]; + let m0 = k0.map(|k0| base.generator() * (k0 + C::Scalar::from_u64(2))); + let m0 = self.witness_point_config.assign_region( + m0.map(|point| point.to_affine()), + offset, + region, + )?; + + // 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 + 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<'_, C::Base>, + offset: usize, + mut acc: EccPoint, + base: OrchardFixedBases, + scalar: &ScalarFixed, + ) -> Result, Error> { + // This is 2^w, where w is the window width + let h = C::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 = + k.map(|k| base_value * (k + C::Scalar::from_u64(2)) * h.pow(&[w as u64, 0, 0, 0])); + let mul_b = self.witness_point_config.assign_region( + mul_b.map(|point| point.to_affine()), + offset + w, + region, + )?; + + // 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<'_, C::Base>, + offset: usize, + base: OrchardFixedBases, + scalar: &ScalarFixed, + ) -> Result, Error> { + // This is 2^w, where w is the window width + let h = C::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(C::ScalarExt::zero(), |acc, w| { + acc + C::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 = scalar.map(|scalar| base.generator() * scalar); + self.witness_point_config.assign_region( + mul_b.map(|point| point.to_affine()), + offset + NUM_WINDOWS - 1, + region, + ) + } +} + +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| C::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 000000000..f7a030429 --- /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::{arithmetic::CurveAffine, circuit::Region, plonk::Error}; + +pub struct Config(super::Config); + +impl From<&EccConfig> for Config { + fn from(config: &EccConfig) -> Self { + Self(config.into()) + } +} + +impl std::ops::Deref for Config { + type Target = super::Config; + + fn deref(&self) -> &super::Config { + &self.0 + } +} + +impl Config { + pub fn assign_region( + &self, + scalar: &EccScalarFixed, + base: OrchardFixedBasesFull, + offset: usize, + region: &mut Region<'_, C::Base>, + ) -> Result, Error> { + let (acc, mul_b) = + (*self).assign_region_inner(region, offset, &scalar.into(), base.into())?; + + // Add to the accumulator and return the final result as `[scalar]B`. + let result = self + .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| C::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::Field; + use halo2::{ + arithmetic::{CurveAffine, FieldExt}, + circuit::Layouter, + plonk::Error, + }; + + use crate::circuit::gadget::ecc::{ + chip::{EccChip, OrchardFixedBasesFull}, + FixedPoint, ScalarFixed, + }; + use crate::constants; + use std::marker::PhantomData; + + pub fn test_mul_fixed( + chip: EccChip, + mut layouter: impl Layouter, + ) -> Result<(), Error> { + // commit_ivk_r + let commit_ivk_r = OrchardFixedBasesFull::CommitIvkR(PhantomData); + 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(PhantomData); + 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(PhantomData); + 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(PhantomData); + let value_commit_r = FixedPoint::from_inner(chip.clone(), value_commit_r); + test_single_base( + chip, + layouter.namespace(|| "value_commit_r"), + value_commit_r, + )?; + + Ok(()) + } + + fn test_single_base( + chip: EccChip, + mut layouter: impl Layouter, + base: FixedPoint>, + ) -> Result<(), Error> { + // [a]B + { + let scalar_fixed = C::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 = C::ScalarExt::from_u64(constants::H as u64); + let scalar_fixed = "1333333333333333333333333333333333333333333333333333333333333333333333333333333333334" + .chars() + .fold(C::ScalarExt::zero(), |acc, c| { + acc * h + C::ScalarExt::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 = C::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 000000000..97b7731b9 --- /dev/null +++ b/src/circuit/gadget/ecc/chip/mul_fixed/short.rs @@ -0,0 +1,246 @@ +use super::super::{copy, CellValue, EccConfig, EccPoint, EccScalarFixedShort, Var}; +use crate::constants::ValueCommitV; + +use halo2::{ + arithmetic::{CurveAffine, Field}, + circuit::Region, + plonk::{ConstraintSystem, Error, Selector}, + poly::Rotation, +}; + +pub struct Config { + // Selector used for fixed-base scalar mul with short signed exponent. + q_mul_fixed_short: Selector, + mul_fixed_config: super::Config, +} + +impl From<&EccConfig> for Config { + fn from(config: &EccConfig) -> Self { + Self { + q_mul_fixed_short: config.q_mul_fixed_short, + mul_fixed_config: config.into(), + } + } +} + +impl std::ops::Deref for Config { + type Target = super::Config; + + fn deref(&self) -> &super::Config { + &self.mul_fixed_config + } +} + +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(super) 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, Rotation::cur()); + let y_p = meta.query_advice(self.y_p, Rotation::cur()); + let y_a = meta.query_advice(self.add_config.y_qr, Rotation::cur()); + let sign = meta.query_advice(self.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; + + [y_check, negation_check] + .iter() + .map(|poly| q_mul_fixed_short.clone() * poly.clone()) + .collect() + }); + } + + pub fn assign_region( + &self, + scalar: &EccScalarFixedShort, + base: &ValueCommitV, + offset: usize, + region: &mut Region<'_, C::Base>, + ) -> Result, Error> { + let (acc, mul_b) = + self.assign_region_inner(region, offset, &scalar.into(), base.clone().into())?; + + // Add to the cumulative sum to get `[magnitude]B`. + let magnitude_mul = + self.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.window, + offset + NUM_WINDOWS, + &scalar.sign, + &self.perm, + )?; + + // Conditionally negate `y`-coordinate + let y_val = if let Some(sign) = sign.value() { + if sign == -C::Base::one() { + magnitude_mul.y.value().map(|y: C::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.x_p, + offset + NUM_WINDOWS, + || x_val.ok_or(Error::SynthesisError), + )?; + let y_var = region.assign_advice( + || "y_var", + self.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 == C::Base::one() { + C::Scalar::one() + } else if sign == -C::Base::one() { + -C::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::Field; + use halo2::{ + arithmetic::{CurveAffine, FieldExt}, + circuit::Layouter, + plonk::Error, + }; + + use crate::circuit::gadget::ecc::{chip::EccChip, FixedPointShort, ScalarFixedShort}; + use crate::constants::load::ValueCommitV; + + pub fn test_mul_fixed_short( + chip: EccChip, + mut layouter: impl Layouter, + ) -> Result<(), Error> { + // 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 = C::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 = C::Scalar::from_u64(rand::random::()); + let mut sign = C::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 = C::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 = -C::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 = C::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(()) + } +} diff --git a/src/circuit/gadget/ecc/chip/witness_point.rs b/src/circuit/gadget/ecc/chip/witness_point.rs new file mode 100644 index 000000000..acbb05058 --- /dev/null +++ b/src/circuit/gadget/ecc/chip/witness_point.rs @@ -0,0 +1,78 @@ +use super::{CellValue, EccConfig, EccPoint, Var}; + +use halo2::{ + arithmetic::CurveAffine, + circuit::Region, + plonk::{Advice, Column, ConstraintSystem, Error, Expression, Selector}, + poly::Rotation, +}; + +#[derive(Clone, Debug)] +pub struct Config { + q_point: Selector, + // x-coordinate + pub x: Column, + // y-coordinate + pub y: Column, +} + +impl From<&EccConfig> for Config { + fn from(ecc_config: &EccConfig) -> Self { + Self { + q_point: ecc_config.q_point, + x: ecc_config.advices[0], + y: ecc_config.advices[1], + } + } +} + +impl Config { + pub(super) fn create_gate(&self, meta: &mut ConstraintSystem) { + meta.create_gate("witness point", |meta| { + let q_point = meta.query_selector(self.q_point, Rotation::cur()); + let x = meta.query_advice(self.x, Rotation::cur()); + let y = meta.query_advice(self.y, Rotation::cur()); + + // Check that y^2 = x^3 + b, where b = 5 in the Pallas equation + vec![ + q_point + * (y.clone() * y - (x.clone() * x.clone() * x) - Expression::Constant(C::b())), + ] + }); + } + + pub(super) fn assign_region( + &self, + value: Option, + offset: usize, + region: &mut Region<'_, C::Base>, + ) -> Result, Error> { + // Enable `q_point` selector + self.q_point.enable(region, offset)?; + + let value = value.map(|value| value.coordinates().unwrap()); + + // Assign `x` value + let x_val = value.map(|value| *value.x()); + let x_var = region.assign_advice( + || "x", + self.x, + offset, + || x_val.ok_or(Error::SynthesisError), + )?; + + // Assign `y` value + let y_val = value.map(|value| *value.y()); + let y_var = region.assign_advice( + || "y", + self.y, + offset, + || y_val.ok_or(Error::SynthesisError), + )?; + + Ok(EccPoint { + x: CellValue::::new(x_var, x_val), + y: CellValue::::new(y_var, y_val), + }) + } +} diff --git a/src/circuit/gadget/ecc/chip/witness_scalar_fixed.rs b/src/circuit/gadget/ecc/chip/witness_scalar_fixed.rs new file mode 100644 index 000000000..458d66e11 --- /dev/null +++ b/src/circuit/gadget/ecc/chip/witness_scalar_fixed.rs @@ -0,0 +1,94 @@ +use super::{CellValue, EccConfig, Var}; +use crate::constants::{self, util}; +use arrayvec::ArrayVec; +use ff::Field; +use halo2::{ + arithmetic::{CurveAffine, FieldExt}, + circuit::Region, + plonk::{Advice, Column, ConstraintSystem, Error, Expression, Selector}, + poly::Rotation, +}; +use std::marker::PhantomData; + +pub mod full_width; +pub mod short; + +pub struct Config { + q_scalar_fixed: Selector, + // Decomposition of scalar into `k`-bit windows. + window: Column, + _marker: PhantomData, +} + +impl From<&EccConfig> for Config { + fn from(ecc_config: &EccConfig) -> Self { + Self { + q_scalar_fixed: ecc_config.q_scalar_fixed, + window: ecc_config.advices[0], + _marker: PhantomData, + } + } +} + +impl Config { + pub(super) fn create_gate(&self, meta: &mut ConstraintSystem) { + // Range check gate applies to both full-width and short scalars. + // Check that `k` is within the allowed window size + meta.create_gate("witness scalar fixed", |meta| { + let q_scalar_fixed = meta.query_selector(self.q_scalar_fixed, Rotation::cur()); + let window = meta.query_advice(self.window, Rotation::cur()); + + let range_check = (0..constants::H) + .fold(Expression::Constant(C::Base::one()), |acc, i| { + acc * (window.clone() - Expression::Constant(C::Base::from_u64(i as u64))) + }); + vec![q_scalar_fixed * range_check] + }); + } + + fn decompose_scalar_fixed( + &self, + scalar: Option, + offset: usize, + region: &mut Region<'_, C::Base>, + ) -> Result, NUM_WINDOWS>, Error> { + // Enable `q_scalar_fixed` selector + for idx in 0..NUM_WINDOWS { + self.q_scalar_fixed.enable(region, offset + idx)?; + } + + // Decompose scalar into `k-bit` windows + let scalar_windows: Option> = scalar.map(|scalar| { + util::decompose_scalar_fixed::( + scalar, + SCALAR_NUM_BITS, + constants::FIXED_BASE_WINDOW_SIZE, + ) + }); + + // Store the scalar decomposition + let mut windows: ArrayVec, NUM_WINDOWS> = ArrayVec::new(); + + let scalar_windows: Vec> = if let Some(windows) = scalar_windows { + assert_eq!(windows.len(), NUM_WINDOWS); + windows + .into_iter() + .map(|window| Some(C::Base::from_u64(window as u64))) + .collect() + } else { + vec![None; NUM_WINDOWS] + }; + + for (idx, window) in scalar_windows.into_iter().enumerate() { + let window_cell = region.assign_advice( + || format!("k[{:?}]", offset + idx), + self.window, + offset + idx, + || window.ok_or(Error::SynthesisError), + )?; + windows.push(CellValue::new(window_cell, window)); + } + + Ok(windows) + } +} diff --git a/src/circuit/gadget/ecc/chip/witness_scalar_fixed/full_width.rs b/src/circuit/gadget/ecc/chip/witness_scalar_fixed/full_width.rs new file mode 100644 index 000000000..984e949e5 --- /dev/null +++ b/src/circuit/gadget/ecc/chip/witness_scalar_fixed/full_width.rs @@ -0,0 +1,33 @@ +use super::super::{EccConfig, EccScalarFixed}; +use crate::constants::{L_ORCHARD_SCALAR, NUM_WINDOWS}; +use halo2::{arithmetic::CurveAffine, circuit::Region, plonk::Error}; + +pub struct Config(super::Config); + +impl From<&EccConfig> for Config { + fn from(ecc_config: &EccConfig) -> Self { + Self(ecc_config.into()) + } +} + +impl std::ops::Deref for Config { + type Target = super::Config; + + fn deref(&self) -> &super::Config { + &self.0 + } +} + +impl Config { + pub fn assign_region( + &self, + value: Option, + offset: usize, + region: &mut Region<'_, C::Base>, + ) -> Result, Error> { + let windows = + self.decompose_scalar_fixed::(value, offset, region)?; + + Ok(EccScalarFixed { value, windows }) + } +} diff --git a/src/circuit/gadget/ecc/chip/witness_scalar_fixed/short.rs b/src/circuit/gadget/ecc/chip/witness_scalar_fixed/short.rs new file mode 100644 index 000000000..1ace112ae --- /dev/null +++ b/src/circuit/gadget/ecc/chip/witness_scalar_fixed/short.rs @@ -0,0 +1,99 @@ +use super::super::{CellValue, EccConfig, EccScalarFixedShort, Var}; +use crate::constants::{L_VALUE, NUM_WINDOWS_SHORT}; +use halo2::{ + arithmetic::{CurveAffine, Field, FieldExt}, + circuit::Region, + plonk::{ConstraintSystem, Error, Expression, Selector}, + poly::Rotation, +}; + +pub struct Config { + q_scalar_fixed_short: Selector, + witness_scalar_fixed_config: super::Config, +} + +impl From<&EccConfig> for Config { + fn from(ecc_config: &EccConfig) -> Self { + Self { + q_scalar_fixed_short: ecc_config.q_scalar_fixed_short, + witness_scalar_fixed_config: ecc_config.into(), + } + } +} + +impl std::ops::Deref for Config { + type Target = super::Config; + + fn deref(&self) -> &super::Config { + &self.witness_scalar_fixed_config + } +} + +impl Config { + pub fn create_gate(&self, meta: &mut ConstraintSystem) { + // Check that sign is either 1 or -1. + meta.create_gate("check sign", |meta| { + let q_scalar_fixed_short = + meta.query_selector(self.q_scalar_fixed_short, Rotation::cur()); + let sign = meta.query_advice(self.window, Rotation::cur()); + + vec![ + q_scalar_fixed_short + * (sign.clone() + Expression::Constant(C::Base::one())) + * (sign - Expression::Constant(C::Base::one())), + ] + }); + } +} + +impl Config { + pub fn assign_region( + &self, + value: Option, + offset: usize, + region: &mut Region<'_, C::Base>, + ) -> Result, Error> { + // Enable `q_scalar_fixed_short` + self.q_scalar_fixed_short + .enable(region, offset + NUM_WINDOWS_SHORT)?; + + // Compute the scalar's sign and magnitude + let sign = value.map(|value| { + // t = (p - 1)/2 + let t = (C::Scalar::zero() - C::Scalar::one()) * C::Scalar::TWO_INV; + if value > t { + -C::Scalar::one() + } else { + C::Scalar::one() + } + }); + + let magnitude = sign.zip(value).map(|(sign, value)| sign * value); + + // Decompose magnitude into `k`-bit windows + let windows = + self.decompose_scalar_fixed::(magnitude, offset, region)?; + + // Assign the sign and enable `q_scalar_fixed_short` + let sign = sign.map(|sign| { + assert!(sign == C::Scalar::one() || sign == -C::Scalar::one()); + if sign == C::Scalar::one() { + C::Base::one() + } else { + -C::Base::one() + } + }); + let sign_cell = region.assign_advice( + || "sign", + self.window, + NUM_WINDOWS_SHORT, + || sign.ok_or(Error::SynthesisError), + )?; + + Ok(EccScalarFixedShort { + magnitude, + sign: CellValue::::new(sign_cell, sign), + windows, + }) + } +} diff --git a/src/constants.rs b/src/constants.rs index 22a556703..79165d688 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -13,11 +13,17 @@ pub mod nullifier_k; pub mod value_commit_r; pub mod value_commit_v; +pub mod load; pub mod util; +pub use load::{OrchardFixedBase, OrchardFixedBasesFull, ValueCommitV}; + /// $\ell^\mathsf{Orchard}_\mathsf{base}$ pub(crate) const L_ORCHARD_BASE: usize = 255; +/// $\ell^\mathsf{Orchard}_\mathsf{scalar}$ +pub(crate) const L_ORCHARD_SCALAR: usize = 255; + /// $\ell_\mathsf{value}$ pub(crate) const L_VALUE: usize = 64; @@ -58,234 +64,181 @@ pub const NUM_WINDOWS_SHORT: usize = /// scalar multiplication pub const NUM_COMPLETE_BITS: usize = 3; -#[derive(Copy, Clone, Debug)] -pub struct CommitIvkR(pub OrchardFixedBase); - -#[derive(Copy, Clone, Debug)] -pub struct NoteCommitR(pub OrchardFixedBase); - -#[derive(Copy, Clone, Debug)] -pub struct NullifierK(pub OrchardFixedBase); - -#[derive(Copy, Clone, Debug)] -pub struct ValueCommitR(pub OrchardFixedBase); - -#[derive(Copy, Clone, Debug)] -pub struct ValueCommitV(pub OrchardFixedBase); - -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub struct OrchardFixedBase(C); - -impl OrchardFixedBase { - pub fn new(generator: C) -> Self { - OrchardFixedBase(generator) - } - - pub fn value(&self) -> C { - self.0 - } -} - -pub trait FixedBase { - /// For each fixed base, we calculate its scalar multiples in three-bit windows. - /// Each window will have $2^3 = 8$ points. - fn compute_window_table(&self, num_windows: usize) -> Vec<[C; H]>; - - /// For each window, we interpolate the $x$-coordinate. - /// Here, we pre-compute and store the coefficients of the interpolation polynomial. - fn compute_lagrange_coeffs(&self, num_windows: usize) -> Vec<[C::Base; H]>; - - /// For each window, $z$ is a field element such that for each point $(x, y)$ in the window: - /// - $z + y = u^2$ (some square in the field); and - /// - $z - y$ is not a square. - /// If successful, return a vector of `(z: u64, us: [C::Base; H])` for each window. - fn find_zs_and_us(&self, num_windows: usize) -> Option>; -} - -impl FixedBase for OrchardFixedBase { - fn compute_window_table(&self, num_windows: usize) -> Vec<[C; H]> { - let mut window_table: Vec<[C; H]> = Vec::with_capacity(num_windows); - - // Generate window table entries for all windows but the last. - // For these first `num_windows - 1` windows, we compute the multiple [(k+2)*(2^3)^w]B. - // Here, w ranges from [0..`num_windows - 1`) - for w in 0..(num_windows - 1) { - window_table.push( - (0..H) - .map(|k| { - // scalar = (k+2)*(8^w) - let scalar = C::ScalarExt::from_u64(k as u64 + 2) - * C::ScalarExt::from_u64(H as u64).pow(&[w as u64, 0, 0, 0]); - (self.0 * scalar).to_affine() - }) - .collect::>() - .into_inner() - .unwrap(), - ); - } +/// For each fixed base, we calculate its scalar multiples in three-bit windows. +/// Each window will have $2^3 = 8$ points. +fn compute_window_table(base: C, num_windows: usize) -> Vec<[C; H]> { + let mut window_table: Vec<[C; H]> = Vec::with_capacity(num_windows); - // Generate window table entries for the last window, w = `num_windows - 1`. - // For the last window, we compute [k * (2^3)^w - sum]B, where sum is defined - // as sum = \sum_{j = 0}^{`num_windows - 2`} 2^{3j+1} - let sum = (0..(num_windows - 1)).fold(C::ScalarExt::zero(), |acc, j| { - acc + C::ScalarExt::from_u64(2).pow(&[ - FIXED_BASE_WINDOW_SIZE as u64 * j as u64 + 1, - 0, - 0, - 0, - ]) - }); + // Generate window table entries for all windows but the last. + // For these first `num_windows - 1` windows, we compute the multiple [(k+2)*(2^3)^w]B. + // Here, w ranges from [0..`num_windows - 1`) + for w in 0..(num_windows - 1) { window_table.push( (0..H) .map(|k| { - // scalar = k * (2^3)^w - sum, where w = `num_windows - 1` - let scalar = C::ScalarExt::from_u64(k as u64) - * C::ScalarExt::from_u64(H as u64).pow(&[ - (num_windows - 1) as u64, - 0, - 0, - 0, - ]) - - sum; - (self.0 * scalar).to_affine() + // scalar = (k+2)*(8^w) + let scalar = C::ScalarExt::from_u64(k as u64 + 2) + * C::ScalarExt::from_u64(H as u64).pow(&[w as u64, 0, 0, 0]); + (base * scalar).to_affine() }) .collect::>() .into_inner() .unwrap(), ); - - window_table } - fn compute_lagrange_coeffs(&self, num_windows: usize) -> Vec<[C::Base; H]> { - // We are interpolating over the 3-bit window, k \in [0..8) - let points: Vec<_> = (0..H).map(|i| C::Base::from_u64(i as u64)).collect(); + // Generate window table entries for the last window, w = `num_windows - 1`. + // For the last window, we compute [k * (2^3)^w - sum]B, where sum is defined + // as sum = \sum_{j = 0}^{`num_windows - 2`} 2^{3j+1} + let sum = (0..(num_windows - 1)).fold(C::ScalarExt::zero(), |acc, j| { + acc + C::ScalarExt::from_u64(2).pow(&[ + FIXED_BASE_WINDOW_SIZE as u64 * j as u64 + 1, + 0, + 0, + 0, + ]) + }); + window_table.push( + (0..H) + .map(|k| { + // scalar = k * (2^3)^w - sum, where w = `num_windows - 1` + let scalar = C::ScalarExt::from_u64(k as u64) + * C::ScalarExt::from_u64(H as u64).pow(&[(num_windows - 1) as u64, 0, 0, 0]) + - sum; + (base * scalar).to_affine() + }) + .collect::>() + .into_inner() + .unwrap(), + ); - let window_table = self.compute_window_table(num_windows); + window_table +} - window_table - .iter() - .map(|window_points| { - let x_window_points: Vec<_> = window_points - .iter() - .map(|point| *point.coordinates().unwrap().x()) - .collect(); - lagrange_interpolate(&points, &x_window_points) - .into_iter() - .collect::>() - .into_inner() - .unwrap() - }) - .collect() - } +/// For each window, we interpolate the $x$-coordinate. +/// Here, we pre-compute and store the coefficients of the interpolation polynomial. +fn compute_lagrange_coeffs(base: C, num_windows: usize) -> Vec<[C::Base; H]> { + // We are interpolating over the 3-bit window, k \in [0..8) + let points: Vec<_> = (0..H).map(|i| C::Base::from_u64(i as u64)).collect(); - /// For each window, z is a field element such that for each point (x, y) in the window: - /// - z + y = u^2 (some square in the field); and - /// - z - y is not a square. - /// If successful, return a vector of `(z: u64, us: [C::Base; H])` for each window. - fn find_zs_and_us(&self, num_windows: usize) -> Option> { - // Closure to find z and u's for one window - let find_z_and_us = |window_points: &[C]| { - assert_eq!(H, window_points.len()); + let window_table = compute_window_table(base, num_windows); - let ys: Vec<_> = window_points + window_table + .iter() + .map(|window_points| { + let x_window_points: Vec<_> = window_points .iter() - .map(|point| *point.coordinates().unwrap().y()) + .map(|point| *point.coordinates().unwrap().x()) .collect(); - (0..(1000 * (1 << (2 * H)))).find_map(|z| { - ys.iter() - .map(|&y| { - if (-y + C::Base::from_u64(z)).sqrt().is_none().into() { - (y + C::Base::from_u64(z)).sqrt().into() - } else { - None - } - }) - .collect::>>() - .map(|us| (z, us.into_inner().unwrap())) - }) - }; - - let window_table = self.compute_window_table(num_windows); - window_table - .iter() - .map(|window_points| find_z_and_us(window_points)) - .collect() - } + lagrange_interpolate(&points, &x_window_points) + .into_iter() + .collect::>() + .into_inner() + .unwrap() + }) + .collect() } -trait TestFixedBase { - // Test that Lagrange interpolation coefficients reproduce the correct x-coordinate - // for each fixed-base multiple in each window. - fn test_lagrange_coeffs(&self, num_windows: usize); +/// For each window, $z$ is a field element such that for each point $(x, y)$ in the window: +/// - $z + y = u^2$ (some square in the field); and +/// - $z - y$ is not a square. +/// If successful, return a vector of `(z: u64, us: [C::Base; H])` for each window. +fn find_zs_and_us(base: C, num_windows: usize) -> Option> { + // Closure to find z and u's for one window + let find_z_and_us = |window_points: &[C]| { + assert_eq!(H, window_points.len()); - // Test that the z-values and u-values satisfy the conditions: - // 1. z + y = u^2, - // 2. z - y is not a square - // for the y-coordinate of each fixed-base multiple in each window. - fn test_zs_and_us(&self, z: &[u64], u: &[[[u8; 32]; H]], num_windows: usize); + let ys: Vec<_> = window_points + .iter() + .map(|point| *point.coordinates().unwrap().y()) + .collect(); + (0..(1000 * (1 << (2 * H)))).find_map(|z| { + ys.iter() + .map(|&y| { + if (-y + C::Base::from_u64(z)).sqrt().is_none().into() { + (y + C::Base::from_u64(z)).sqrt().into() + } else { + None + } + }) + .collect::>>() + .map(|us| (z, us.into_inner().unwrap())) + }) + }; + + let window_table = compute_window_table(base, num_windows); + window_table + .iter() + .map(|window_points| find_z_and_us(window_points)) + .collect() } -impl TestFixedBase for OrchardFixedBase { - fn test_lagrange_coeffs(&self, num_windows: usize) { - let lagrange_coeffs = self.compute_lagrange_coeffs(num_windows); - - // Check first 84 windows, i.e. `k_0, k_1, ..., k_83` - for (idx, coeffs) in lagrange_coeffs[0..(num_windows - 1)].iter().enumerate() { - // Test each three-bit chunk in this window. - for bits in 0..(1 << FIXED_BASE_WINDOW_SIZE) { - { - // Interpolate the x-coordinate using this window's coefficients - let interpolated_x = util::evaluate::(bits, coeffs); - - // Compute the actual x-coordinate of the multiple [(k+2)*(8^w)]B. - let point = self.0 - * C::Scalar::from_u64(bits as u64 + 2) - * C::Scalar::from_u64(H as u64).pow(&[idx as u64, 0, 0, 0]); - let x = *point.to_affine().coordinates().unwrap().x(); - - // Check that the interpolated x-coordinate matches the actual one. - assert_eq!(x, interpolated_x); - } - } - } +#[cfg(test)] +// Test that Lagrange interpolation coefficients reproduce the correct x-coordinate +// for each fixed-base multiple in each window. +fn test_lagrange_coeffs(base: C, num_windows: usize) { + let lagrange_coeffs = compute_lagrange_coeffs(base, num_windows); - // Check last window. + // Check first 84 windows, i.e. `k_0, k_1, ..., k_83` + for (idx, coeffs) in lagrange_coeffs[0..(num_windows - 1)].iter().enumerate() { + // Test each three-bit chunk in this window. for bits in 0..(1 << FIXED_BASE_WINDOW_SIZE) { - // Interpolate the x-coordinate using the last window's coefficients - let interpolated_x = util::evaluate::(bits, &lagrange_coeffs[num_windows - 1]); - - // Compute the actual x-coordinate of the multiple [k * (8^84) - offset]B, - // where offset = \sum_{j = 0}^{83} 2^{3j+1} - let offset = (0..(num_windows - 1)).fold(C::Scalar::zero(), |acc, w| { - acc + C::Scalar::from_u64(2).pow(&[ - FIXED_BASE_WINDOW_SIZE as u64 * w as u64 + 1, - 0, - 0, - 0, - ]) - }); - let scalar = C::Scalar::from_u64(bits as u64) - * C::Scalar::from_u64(H as u64).pow(&[(num_windows - 1) as u64, 0, 0, 0]) - - offset; - let point = self.0 * scalar; - let x = *point.to_affine().coordinates().unwrap().x(); - - // Check that the interpolated x-coordinate matches the actual one. - assert_eq!(x, interpolated_x); + { + // Interpolate the x-coordinate using this window's coefficients + let interpolated_x = util::evaluate::(bits, coeffs); + + // Compute the actual x-coordinate of the multiple [(k+2)*(8^w)]B. + let point = base + * C::Scalar::from_u64(bits as u64 + 2) + * C::Scalar::from_u64(H as u64).pow(&[idx as u64, 0, 0, 0]); + let x = *point.to_affine().coordinates().unwrap().x(); + + // Check that the interpolated x-coordinate matches the actual one. + assert_eq!(x, interpolated_x); + } } } - fn test_zs_and_us(&self, z: &[u64], u: &[[[u8; 32]; H]], num_windows: usize) { - let window_table = self.compute_window_table(num_windows); + // Check last window. + for bits in 0..(1 << FIXED_BASE_WINDOW_SIZE) { + // Interpolate the x-coordinate using the last window's coefficients + let interpolated_x = util::evaluate::(bits, &lagrange_coeffs[num_windows - 1]); - for ((u, z), window_points) in u.iter().zip(z.iter()).zip(window_table) { - for (u, point) in u.iter().zip(window_points.iter()) { - let y = *point.coordinates().unwrap().y(); - let u = C::Base::from_bytes(&u).unwrap(); - assert_eq!(C::Base::from_u64(*z) + y, u * u); // allow either square root - assert!(bool::from((C::Base::from_u64(*z) - y).sqrt().is_none())); - } + // Compute the actual x-coordinate of the multiple [k * (8^84) - offset]B, + // where offset = \sum_{j = 0}^{83} 2^{3j+1} + let offset = (0..(num_windows - 1)).fold(C::Scalar::zero(), |acc, w| { + acc + C::Scalar::from_u64(2).pow(&[ + FIXED_BASE_WINDOW_SIZE as u64 * w as u64 + 1, + 0, + 0, + 0, + ]) + }); + let scalar = C::Scalar::from_u64(bits as u64) + * C::Scalar::from_u64(H as u64).pow(&[(num_windows - 1) as u64, 0, 0, 0]) + - offset; + let point = base * scalar; + let x = *point.to_affine().coordinates().unwrap().x(); + + // Check that the interpolated x-coordinate matches the actual one. + assert_eq!(x, interpolated_x); + } +} + +#[cfg(test)] +// Test that the z-values and u-values satisfy the conditions: +// 1. z + y = u^2, +// 2. z - y is not a square +// for the y-coordinate of each fixed-base multiple in each window. +fn test_zs_and_us(base: C, z: &[u64], u: &[[[u8; 32]; H]], num_windows: usize) { + let window_table = compute_window_table(base, num_windows); + + for ((u, z), window_points) in u.iter().zip(z.iter()).zip(window_table) { + for (u, point) in u.iter().zip(window_points.iter()) { + let y = *point.coordinates().unwrap().y(); + let u = C::Base::from_bytes(&u).unwrap(); + assert_eq!(C::Base::from_u64(*z) + y, u * u); // allow either square root + assert!(bool::from((C::Base::from_u64(*z) - y).sqrt().is_none())); } } } diff --git a/src/constants/commit_ivk_r.rs b/src/constants/commit_ivk_r.rs index cc539008e..90c5f16c5 100644 --- a/src/constants/commit_ivk_r.rs +++ b/src/constants/commit_ivk_r.rs @@ -1,4 +1,3 @@ -use super::{CommitIvkR, OrchardFixedBase}; use halo2::arithmetic::{CurveAffine, FieldExt}; /// Generator used in SinsemillaCommit randomness for IVK commitment @@ -2918,19 +2917,19 @@ pub const U: [[[u8; 32]; super::H]; super::NUM_WINDOWS] = [ ], ]; -pub fn generator() -> CommitIvkR { - CommitIvkR(OrchardFixedBase::::new( - C::from_xy( - C::Base::from_bytes(&GENERATOR.0).unwrap(), - C::Base::from_bytes(&GENERATOR.1).unwrap(), - ) - .unwrap(), - )) +pub fn generator() -> C { + C::from_xy( + C::Base::from_bytes(&GENERATOR.0).unwrap(), + C::Base::from_bytes(&GENERATOR.1).unwrap(), + ) + .unwrap() } #[cfg(test)] mod tests { - use super::super::{TestFixedBase, COMMIT_IVK_PERSONALIZATION, NUM_WINDOWS}; + use super::super::{ + test_lagrange_coeffs, test_zs_and_us, COMMIT_IVK_PERSONALIZATION, NUM_WINDOWS, + }; use super::*; use crate::primitives::sinsemilla::CommitDomain; use group::Curve; @@ -2952,12 +2951,12 @@ mod tests { #[test] fn lagrange_coeffs() { let base = super::generator::(); - base.0.test_lagrange_coeffs(NUM_WINDOWS); + test_lagrange_coeffs(base, NUM_WINDOWS); } #[test] fn z() { let base = super::generator::(); - base.0.test_zs_and_us(&Z, &U, NUM_WINDOWS); + test_zs_and_us(base, &Z, &U, NUM_WINDOWS); } } diff --git a/src/constants/load.rs b/src/constants/load.rs new file mode 100644 index 000000000..6f50724d8 --- /dev/null +++ b/src/constants/load.rs @@ -0,0 +1,209 @@ +use std::convert::TryInto; + +use crate::constants::{self, compute_lagrange_coeffs, H, NUM_WINDOWS, NUM_WINDOWS_SHORT}; +use halo2::arithmetic::{CurveAffine, FieldExt}; +use std::marker::PhantomData; + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum OrchardFixedBasesFull { + CommitIvkR(PhantomData), + NoteCommitR(PhantomData), + NullifierK(PhantomData), + ValueCommitR(PhantomData), +} + +/// A fixed base to be used in scalar multiplication with a full-width scalar. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct OrchardFixedBase { + pub generator: C, + pub lagrange_coeffs: LagrangeCoeffs, + pub z: Z, + pub u: U, +} + +impl From> for OrchardFixedBase { + fn from(base: OrchardFixedBasesFull) -> Self { + let (generator, z, u) = match base { + OrchardFixedBasesFull::CommitIvkR(_) => ( + super::commit_ivk_r::generator(), + super::commit_ivk_r::Z.into(), + super::commit_ivk_r::U.into(), + ), + OrchardFixedBasesFull::NoteCommitR(_) => ( + super::note_commit_r::generator(), + super::note_commit_r::Z.into(), + super::note_commit_r::U.into(), + ), + OrchardFixedBasesFull::NullifierK(_) => ( + super::nullifier_k::generator(), + super::nullifier_k::Z.into(), + super::nullifier_k::U.into(), + ), + OrchardFixedBasesFull::ValueCommitR(_) => ( + super::value_commit_r::generator(), + super::value_commit_r::Z.into(), + super::value_commit_r::U.into(), + ), + }; + + Self { + generator, + lagrange_coeffs: compute_lagrange_coeffs(generator, NUM_WINDOWS).into(), + z, + u, + } + } +} + +/// A fixed base to be used in scalar multiplication with a short signed exponent. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ValueCommitV { + pub generator: C, + pub lagrange_coeffs_short: LagrangeCoeffsShort, + pub z_short: ZShort, + pub u_short: UShort, +} + +impl ValueCommitV { + pub fn get() -> Self { + let generator = super::value_commit_v::generator(); + Self { + generator, + lagrange_coeffs_short: compute_lagrange_coeffs(generator, NUM_WINDOWS_SHORT).into(), + z_short: super::value_commit_v::Z_SHORT.into(), + u_short: super::value_commit_v::U_SHORT.into(), + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +// 8 coefficients per window +pub struct WindowLagrangeCoeffs(pub Box<[F; H]>); + +impl From<&[F; H]> for WindowLagrangeCoeffs { + fn from(array: &[F; H]) -> Self { + Self(Box::new(*array)) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +// 85 windows per base (with the exception of ValueCommitV) +pub struct LagrangeCoeffs(pub Box<[WindowLagrangeCoeffs; constants::NUM_WINDOWS]>); + +impl From>> for LagrangeCoeffs { + fn from(windows: Vec>) -> Self { + Self(windows.into_boxed_slice().try_into().unwrap()) + } +} + +impl From> for LagrangeCoeffs { + fn from(arrays: Vec<[F; H]>) -> Self { + let windows: Vec> = + arrays.iter().map(|array| array.into()).collect(); + windows.into() + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +// 22 windows for ValueCommitV +pub struct LagrangeCoeffsShort(pub Box<[WindowLagrangeCoeffs; NUM_WINDOWS_SHORT]>); + +impl From>> for LagrangeCoeffsShort { + fn from(windows: Vec>) -> Self { + Self(windows.into_boxed_slice().try_into().unwrap()) + } +} + +impl From> for LagrangeCoeffsShort { + fn from(arrays: Vec<[F; H]>) -> Self { + let windows: Vec> = + arrays.iter().map(|array| array.into()).collect(); + windows.into() + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +// 85 Z's per base (with the exception of ValueCommitV) +pub struct Z(pub Box<[F; NUM_WINDOWS]>); + +impl From<[u64; NUM_WINDOWS]> for Z { + fn from(zs: [u64; NUM_WINDOWS]) -> Self { + Self( + zs.iter() + .map(|z| F::from_u64(*z)) + .collect::>() + .into_boxed_slice() + .try_into() + .unwrap(), + ) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +// 22 Z's for ValueCommitV +pub struct ZShort(pub Box<[F; NUM_WINDOWS_SHORT]>); + +impl From<[u64; NUM_WINDOWS_SHORT]> for ZShort { + fn from(zs: [u64; NUM_WINDOWS_SHORT]) -> Self { + Self( + zs.iter() + .map(|z| F::from_u64(*z)) + .collect::>() + .into_boxed_slice() + .try_into() + .unwrap(), + ) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +// 8 u's per window +pub struct WindowUs(pub Box<[F; H]>); + +impl From<&[[u8; 32]; H]> for WindowUs { + fn from(window_us: &[[u8; 32]; H]) -> Self { + Self( + window_us + .iter() + .map(|u| F::from_bytes(&u).unwrap()) + .collect::>() + .into_boxed_slice() + .try_into() + .unwrap(), + ) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +// 85 windows per base (with the exception of ValueCommitV) +pub struct U(pub Box<[WindowUs; NUM_WINDOWS]>); + +impl From>> for U { + fn from(windows: Vec>) -> Self { + Self(windows.into_boxed_slice().try_into().unwrap()) + } +} + +impl From<[[[u8; 32]; H]; NUM_WINDOWS]> for U { + fn from(window_us: [[[u8; 32]; H]; NUM_WINDOWS]) -> Self { + let windows: Vec> = window_us.iter().map(|us| us.into()).collect(); + windows.into() + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +// 22 windows for ValueCommitV +pub struct UShort(pub Box<[WindowUs; NUM_WINDOWS_SHORT]>); + +impl From>> for UShort { + fn from(windows: Vec>) -> Self { + Self(windows.into_boxed_slice().try_into().unwrap()) + } +} + +impl From<[[[u8; 32]; H]; NUM_WINDOWS_SHORT]> for UShort { + fn from(window_us: [[[u8; 32]; H]; NUM_WINDOWS_SHORT]) -> Self { + let windows: Vec> = window_us.iter().map(|us| us.into()).collect(); + windows.into() + } +} diff --git a/src/constants/note_commit_r.rs b/src/constants/note_commit_r.rs index 3e9c59ebc..ecae83d42 100644 --- a/src/constants/note_commit_r.rs +++ b/src/constants/note_commit_r.rs @@ -1,4 +1,3 @@ -use super::{NoteCommitR, OrchardFixedBase}; use halo2::arithmetic::{CurveAffine, FieldExt}; /// Generator used in SinsemillaCommit randomness for note commitment @@ -2918,19 +2917,19 @@ pub const U: [[[u8; 32]; super::H]; super::NUM_WINDOWS] = [ ], ]; -pub fn generator() -> NoteCommitR { - NoteCommitR(OrchardFixedBase::::new( - C::from_xy( - C::Base::from_bytes(&GENERATOR.0).unwrap(), - C::Base::from_bytes(&GENERATOR.1).unwrap(), - ) - .unwrap(), - )) +pub fn generator() -> C { + C::from_xy( + C::Base::from_bytes(&GENERATOR.0).unwrap(), + C::Base::from_bytes(&GENERATOR.1).unwrap(), + ) + .unwrap() } #[cfg(test)] mod tests { - use super::super::{TestFixedBase, NOTE_COMMITMENT_PERSONALIZATION, NUM_WINDOWS}; + use super::super::{ + test_lagrange_coeffs, test_zs_and_us, NOTE_COMMITMENT_PERSONALIZATION, NUM_WINDOWS, + }; use super::*; use crate::primitives::sinsemilla::CommitDomain; use group::Curve; @@ -2952,12 +2951,12 @@ mod tests { #[test] fn lagrange_coeffs() { let base = super::generator::(); - base.0.test_lagrange_coeffs(NUM_WINDOWS); + test_lagrange_coeffs(base, NUM_WINDOWS); } #[test] fn z() { let base = super::generator::(); - base.0.test_zs_and_us(&Z, &U, NUM_WINDOWS); + test_zs_and_us(base, &Z, &U, NUM_WINDOWS); } } diff --git a/src/constants/nullifier_k.rs b/src/constants/nullifier_k.rs index 15971dfb2..dff07bcc8 100644 --- a/src/constants/nullifier_k.rs +++ b/src/constants/nullifier_k.rs @@ -1,4 +1,3 @@ -use crate::constants::{NullifierK, OrchardFixedBase}; use halo2::arithmetic::{CurveAffine, FieldExt}; pub const GENERATOR: ([u8; 32], [u8; 32]) = ( @@ -2917,19 +2916,19 @@ pub const U: [[[u8; 32]; super::H]; super::NUM_WINDOWS] = [ ], ]; -pub fn generator() -> NullifierK { - NullifierK(OrchardFixedBase::::new( - C::from_xy( - C::Base::from_bytes(&GENERATOR.0).unwrap(), - C::Base::from_bytes(&GENERATOR.1).unwrap(), - ) - .unwrap(), - )) +pub fn generator() -> C { + C::from_xy( + C::Base::from_bytes(&GENERATOR.0).unwrap(), + C::Base::from_bytes(&GENERATOR.1).unwrap(), + ) + .unwrap() } #[cfg(test)] mod tests { - use super::super::{TestFixedBase, NUM_WINDOWS, ORCHARD_PERSONALIZATION}; + use super::super::{ + test_lagrange_coeffs, test_zs_and_us, NUM_WINDOWS, ORCHARD_PERSONALIZATION, + }; use super::*; use group::Curve; use halo2::{ @@ -2950,12 +2949,12 @@ mod tests { #[test] fn lagrange_coeffs() { let base = super::generator::(); - base.0.test_lagrange_coeffs(NUM_WINDOWS); + test_lagrange_coeffs(base, NUM_WINDOWS); } #[test] fn z() { let base = super::generator::(); - base.0.test_zs_and_us(&Z, &U, NUM_WINDOWS); + test_zs_and_us(base, &Z, &U, NUM_WINDOWS); } } diff --git a/src/constants/value_commit_r.rs b/src/constants/value_commit_r.rs index 9278a11a7..bff418772 100644 --- a/src/constants/value_commit_r.rs +++ b/src/constants/value_commit_r.rs @@ -1,4 +1,3 @@ -use super::{OrchardFixedBase, ValueCommitR}; use halo2::arithmetic::{CurveAffine, FieldExt}; /// The value commitment is used to check balance between inputs and outputs. The value is @@ -2919,19 +2918,19 @@ pub const U: [[[u8; 32]; super::H]; super::NUM_WINDOWS] = [ ], ]; -pub fn generator() -> ValueCommitR { - ValueCommitR(OrchardFixedBase::::new( - C::from_xy( - C::Base::from_bytes(&GENERATOR.0).unwrap(), - C::Base::from_bytes(&GENERATOR.1).unwrap(), - ) - .unwrap(), - )) +pub fn generator() -> C { + C::from_xy( + C::Base::from_bytes(&GENERATOR.0).unwrap(), + C::Base::from_bytes(&GENERATOR.1).unwrap(), + ) + .unwrap() } #[cfg(test)] mod tests { - use super::super::{TestFixedBase, NUM_WINDOWS, VALUE_COMMITMENT_PERSONALIZATION}; + use super::super::{ + test_lagrange_coeffs, test_zs_and_us, NUM_WINDOWS, VALUE_COMMITMENT_PERSONALIZATION, + }; use super::*; use group::Curve; use halo2::{ @@ -2952,12 +2951,12 @@ mod tests { #[test] fn lagrange_coeffs() { let base = super::generator::(); - base.0.test_lagrange_coeffs(NUM_WINDOWS); + test_lagrange_coeffs(base, NUM_WINDOWS); } #[test] fn z() { let base = super::generator::(); - base.0.test_zs_and_us(&Z, &U, NUM_WINDOWS); + test_zs_and_us(base, &Z, &U, NUM_WINDOWS); } } diff --git a/src/constants/value_commit_v.rs b/src/constants/value_commit_v.rs index 2e5d31a61..0ad8661b7 100644 --- a/src/constants/value_commit_v.rs +++ b/src/constants/value_commit_v.rs @@ -1,4 +1,3 @@ -use super::{OrchardFixedBase, ValueCommitV}; use halo2::arithmetic::{CurveAffine, FieldExt}; /// The value commitment is used to check balance between inputs and outputs. The value is @@ -772,19 +771,19 @@ pub const U_SHORT: [[[u8; 32]; super::H]; super::NUM_WINDOWS_SHORT] = [ ], ]; -pub fn generator() -> ValueCommitV { - ValueCommitV(OrchardFixedBase::::new( - C::from_xy( - C::Base::from_bytes(&GENERATOR.0).unwrap(), - C::Base::from_bytes(&GENERATOR.1).unwrap(), - ) - .unwrap(), - )) +pub fn generator() -> C { + C::from_xy( + C::Base::from_bytes(&GENERATOR.0).unwrap(), + C::Base::from_bytes(&GENERATOR.1).unwrap(), + ) + .unwrap() } #[cfg(test)] mod tests { - use super::super::{TestFixedBase, NUM_WINDOWS_SHORT, VALUE_COMMITMENT_PERSONALIZATION}; + use super::super::{ + test_lagrange_coeffs, test_zs_and_us, NUM_WINDOWS_SHORT, VALUE_COMMITMENT_PERSONALIZATION, + }; use super::*; use group::Curve; use halo2::{ @@ -805,12 +804,12 @@ mod tests { #[test] fn lagrange_coeffs_short() { let base = super::generator::(); - base.0.test_lagrange_coeffs(NUM_WINDOWS_SHORT); + test_lagrange_coeffs(base, NUM_WINDOWS_SHORT); } #[test] fn z_short() { let base = super::generator::(); - base.0.test_zs_and_us(&Z_SHORT, &U_SHORT, NUM_WINDOWS_SHORT); + test_zs_and_us(base, &Z_SHORT, &U_SHORT, NUM_WINDOWS_SHORT); } }