diff --git a/halo2_gadgets/src/sinsemilla.rs b/halo2_gadgets/src/sinsemilla.rs index a1dcb9d2..2a9c92a7 100644 --- a/halo2_gadgets/src/sinsemilla.rs +++ b/halo2_gadgets/src/sinsemilla.rs @@ -78,7 +78,7 @@ pub trait SinsemillaInstructions Result<(Self::NonIdentityPoint, Vec), Error>; + /// Hashes a message to an ECC curve point. + /// This returns both the resulting point, as well as the message + /// decomposition in the form of intermediate values in a cumulative + /// sum. + /// The initial point `Q` is a private point. + #[allow(non_snake_case)] + #[allow(clippy::type_complexity)] + fn hash_to_point_with_private_init( + &self, + layouter: impl Layouter, + Q: &Self::NonIdentityPoint, + message: Self::Message, + ) -> Result<(Self::NonIdentityPoint, Vec), Error>; + /// Extracts the x-coordinate of the output of a Sinsemilla hash. fn extract(point: &Self::NonIdentityPoint) -> Self::X; } @@ -329,6 +343,21 @@ where .map(|(point, zs)| (ecc::NonIdentityPoint::from_inner(self.ecc_chip.clone(), point), zs)) } + #[allow(non_snake_case)] + #[allow(clippy::type_complexity)] + /// Evaluate the Sinsemilla hash of `message` from the private initial point `Q`. + pub fn hash_to_point_with_private_init( + &self, + layouter: impl Layouter, + Q: &>::NonIdentityPoint, + message: Message, + ) -> Result<(ecc::NonIdentityPoint, Vec), Error> { + assert_eq!(self.sinsemilla_chip, message.chip); + self.sinsemilla_chip + .hash_to_point_with_private_init(layouter, Q, message.inner) + .map(|(point, zs)| (ecc::NonIdentityPoint::from_inner(self.ecc_chip.clone(), point), zs)) + } + /// $\mathsf{SinsemillaHash}$ from [§ 5.4.1.9][concretesinsemillahash]. /// /// [concretesinsemillahash]: https://zips.z.cash/protocol/protocol.pdf#concretesinsemillahash @@ -431,6 +460,35 @@ where self.M.hash_to_point(layouter, message) } + #[allow(non_snake_case)] + #[allow(clippy::type_complexity)] + /// $\mathsf{SinsemillaCommit}$ from [§ 5.4.8.4][concretesinsemillacommit]. + /// + /// [concretesinsemillacommit]: https://zips.z.cash/protocol/nu5.pdf#concretesinsemillacommit + pub fn hash_with_private_init( + &self, + layouter: impl Layouter, + Q: &>::NonIdentityPoint, + message: Message, + ) -> Result< + ( + ecc::NonIdentityPoint, + Vec, + ), + Error, + > { + assert_eq!(self.M.sinsemilla_chip, message.chip); + self.M.hash_to_point_with_private_init(layouter, Q, message) + } + + #[allow(clippy::type_complexity)] + /// $\mathsf{SinsemillaCommit}$ from [§ 5.4.8.4][concretesinsemillacommit]. + /// + /// [concretesinsemillacommit]: https://zips.z.cash/protocol/nu5.pdf#concretesinsemillacommit + pub fn q_init(&self) -> C { + self.M.Q + } + #[allow(clippy::type_complexity)] /// $\mathsf{SinsemillaCommit}$ from [§ 5.4.8.4][concretesinsemillacommit]. /// diff --git a/halo2_gadgets/src/sinsemilla/chip.rs b/halo2_gadgets/src/sinsemilla/chip.rs index ef76b198..441ba975 100644 --- a/halo2_gadgets/src/sinsemilla/chip.rs +++ b/halo2_gadgets/src/sinsemilla/chip.rs @@ -43,8 +43,11 @@ where /// q_sinsemilla2 is used to define a synthetic selector, /// q_sinsemilla3 = (q_sinsemilla2) ⋅ (q_sinsemilla2 - 1) /// Simple selector used to constrain hash initialization to be consistent with - /// the y-coordinate of the domain $Q$. + /// the y-coordinate of the domain $Q$ when $y_Q$ is a public constant. q_sinsemilla4: Selector, + /// Simple selector used to constrain hash initialization to be consistent with + /// the y-coordinate of the domain $Q$ when $y_Q$ is a private value. + q_sinsemilla4_private: Selector, /// Fixed column used to load the y-coordinate of the domain $Q$. fixed_y_q: Column, /// Logic specific to merged double-and-add. @@ -165,6 +168,7 @@ where q_sinsemilla1: meta.complex_selector(), q_sinsemilla2: meta.fixed_column(), q_sinsemilla4: meta.selector(), + q_sinsemilla4_private: meta.selector(), fixed_y_q, double_and_add: DoubleAndAdd { x_a: advices[0], @@ -202,7 +206,7 @@ where // Check that the initial x_A, x_P, lambda_1, lambda_2 are consistent with y_Q. // https://p.z.cash/halo2-0.1:sinsemilla-constraints?partial - meta.create_gate("Initial y_Q", |meta| { + meta.create_gate("Initial y_Q (public)", |meta| { let q_s4 = meta.query_selector(config.q_sinsemilla4); let y_q = meta.query_fixed(config.fixed_y_q); @@ -215,6 +219,21 @@ where Constraints::with_selector(q_s4, Some(("init_y_q_check", init_y_q_check))) }); + // Check that the initial x_A, x_P, lambda_1, lambda_2 are consistent with y_Q. + // https://p.z.cash/halo2-0.1:sinsemilla-constraints?partial + meta.create_gate("Initial y_Q (private)", |meta| { + let q_s4 = meta.query_selector(config.q_sinsemilla4_private); + let y_q = meta.query_advice(config.double_and_add.x_p, Rotation::prev()); + + // Y_A = (lambda_1 + lambda_2) * (x_a - x_r) + let Y_A_cur = Y_A(meta, Rotation::cur()); + + // 2 * y_q - Y_{A,0} = 0 + let init_y_q_check = y_q * two - Y_A_cur; + + Constraints::with_selector(q_s4, Some(("init_y_q_check", init_y_q_check))) + }); + // https://p.z.cash/halo2-0.1:sinsemilla-constraints?partial meta.create_gate("Sinsemilla gate", |meta| { let q_s1 = meta.query_selector(config.q_sinsemilla1); @@ -322,6 +341,20 @@ where ) } + #[allow(non_snake_case)] + #[allow(clippy::type_complexity)] + fn hash_to_point_with_private_init( + &self, + mut layouter: impl Layouter, + Q: &Self::NonIdentityPoint, + message: Self::Message, + ) -> Result<(Self::NonIdentityPoint, Vec), Error> { + layouter.assign_region( + || "hash_to_point", + |mut region| self.hash_message_with_private_init(&mut region, Q, &message), + ) + } + fn extract(point: &Self::NonIdentityPoint) -> Self::X { point.x() } diff --git a/halo2_gadgets/src/sinsemilla/chip/hash_to_point.rs b/halo2_gadgets/src/sinsemilla/chip/hash_to_point.rs index 44beaa42..771d2a9c 100644 --- a/halo2_gadgets/src/sinsemilla/chip/hash_to_point.rs +++ b/halo2_gadgets/src/sinsemilla/chip/hash_to_point.rs @@ -41,85 +41,9 @@ where ), Error, > { - let config = self.config().clone(); - let mut offset = 0; + let (offset, x_a, y_a) = self.public_initialization(region, Q)?; - // Get the `x`- and `y`-coordinates of the starting `Q` base. - let x_q = *Q.coordinates().unwrap().x(); - let y_q = *Q.coordinates().unwrap().y(); - - // Constrain the initial x_a, lambda_1, lambda_2, x_p using the q_sinsemilla4 - // selector. - let mut y_a: Y = { - // Enable `q_sinsemilla4` on the first row. - config.q_sinsemilla4.enable(region, offset)?; - region.assign_fixed( - || "fixed y_q", - config.fixed_y_q, - offset, - || Value::known(y_q), - )?; - - Value::known(y_q.into()).into() - }; - - // Constrain the initial x_q to equal the x-coordinate of the domain's `Q`. - let mut x_a: X = { - let x_a = region.assign_advice_from_constant( - || "fixed x_q", - config.double_and_add.x_a, - offset, - x_q.into(), - )?; - - x_a.into() - }; - - let mut zs_sum: Vec>> = Vec::new(); - - // Hash each piece in the message. - for (idx, piece) in message.iter().enumerate() { - let final_piece = idx == message.len() - 1; - - // The value of the accumulator after this piece is processed. - let (x, y, zs) = self.hash_piece(region, offset, piece, x_a, y_a, final_piece)?; - - // Since each message word takes one row to process, we increase - // the offset by `piece.num_words` on each iteration. - offset += piece.num_words(); - - // Update the accumulator to the latest value. - x_a = x; - y_a = y; - zs_sum.push(zs); - } - - // Assign the final y_a. - let y_a = { - // Assign the final y_a. - let y_a_cell = - region.assign_advice(|| "y_a", config.double_and_add.lambda_1, offset, || y_a.0)?; - - // Assign lambda_2 and x_p zero values since they are queried - // in the gate. (The actual values do not matter since they are - // multiplied by zero.) - { - region.assign_advice( - || "dummy lambda2", - config.double_and_add.lambda_2, - offset, - || Value::known(pallas::Base::zero()), - )?; - region.assign_advice( - || "dummy x_p", - config.double_and_add.x_p, - offset, - || Value::known(pallas::Base::zero()), - )?; - } - - y_a_cell - }; + let (x_a, y_a, zs_sum) = self.hash_all_pieces(region, offset, message, x_a, y_a)?; #[cfg(test)] #[allow(non_snake_case)] @@ -169,6 +93,239 @@ where )) } + /// [Specification](https://p.z.cash/halo2-0.1:sinsemilla-constraints?partial). + #[allow(non_snake_case)] + #[allow(clippy::type_complexity)] + pub(super) fn hash_message_with_private_init( + &self, + region: &mut Region<'_, pallas::Base>, + Q: &NonIdentityEccPoint, + message: &>::Message, + ) -> Result< + ( + NonIdentityEccPoint, + Vec>>, + ), + Error, + > { + let (offset, x_a, y_a) = self.private_initialization(region, Q)?; + + let (x_a, y_a, zs_sum) = self.hash_all_pieces(region, offset, message, x_a, y_a)?; + + #[cfg(test)] + #[allow(non_snake_case)] + // Check equivalence to result from primitives::sinsemilla::hash_to_point + { + use crate::sinsemilla::primitives::{K, S_PERSONALIZATION}; + + use group::{prime::PrimeCurveAffine, Curve}; + use pasta_curves::arithmetic::CurveExt; + + let field_elems: Value> = message + .iter() + .map(|piece| piece.field_elem().map(|elem| (elem, piece.num_words()))) + .collect(); + + field_elems + .zip(x_a.value().zip(y_a.value())) + .zip(Q.point()) + .assert_if_known(|((field_elems, (x_a, y_a)), Q)| { + // Get message as a bitstring. + let bitstring: Vec = field_elems + .iter() + .flat_map(|(elem, num_words)| { + elem.to_le_bits().into_iter().take(K * num_words) + }) + .collect(); + + let hasher_S = pallas::Point::hash_to_curve(S_PERSONALIZATION); + let S = |chunk: &[bool]| hasher_S(&lebs2ip_k(chunk).to_le_bytes()); + + // We can use complete addition here because it differs from + // incomplete addition with negligible probability. + let expected_point = bitstring + .chunks(K) + .fold(Q.to_curve(), |acc, chunk| (acc + S(chunk)) + acc); + let actual_point = + pallas::Affine::from_xy(x_a.evaluate(), y_a.evaluate()).unwrap(); + expected_point.to_affine() == actual_point + }); + } + + x_a.value() + .zip(y_a.value()) + .error_if_known_and(|(x_a, y_a)| x_a.is_zero_vartime() || y_a.is_zero_vartime())?; + Ok(( + NonIdentityEccPoint::from_coordinates_unchecked(x_a.0, y_a), + zs_sum, + )) + } + + #[allow(non_snake_case)] + /// Assign the coordinates of the initial public point `Q` + /// + /// | offset | x_A | q_sinsemilla4 | fixed_y_Q | + /// -------------------------------------------- + /// | 0 | x_Q | 1 | y_Q | + fn public_initialization( + &self, + region: &mut Region<'_, pallas::Base>, + Q: pallas::Affine, + ) -> Result<(usize, X, Y), Error> { + let config = self.config().clone(); + let offset = 0; + + // Get the `x`- and `y`-coordinates of the starting `Q` base. + let x_q = *Q.coordinates().unwrap().x(); + let y_q = *Q.coordinates().unwrap().y(); + + // Constrain the initial x_a, lambda_1, lambda_2, x_p using the q_sinsemilla4 + // selector. + let y_a: Y = { + // Enable `q_sinsemilla4` on the first row. + config.q_sinsemilla4.enable(region, offset)?; + region.assign_fixed( + || "fixed y_q", + config.fixed_y_q, + offset, + || Value::known(y_q), + )?; + + Value::known(y_q.into()).into() + }; + + // Constrain the initial x_q to equal the x-coordinate of the domain's `Q`. + let x_a: X = { + let x_a = region.assign_advice_from_constant( + || "fixed x_q", + config.double_and_add.x_a, + offset, + x_q.into(), + )?; + + x_a.into() + }; + + Ok((offset, x_a, y_a)) + } + + #[allow(non_snake_case)] + /// Assign the coordinates of the initial private point `Q` + /// + /// | offset | x_A | x_P | q_sinsemilla4_private | + /// ----------------------------------------------- + /// | 0 | | y_Q | | + /// | 1 | x_Q | | 1 | + fn private_initialization( + &self, + region: &mut Region<'_, pallas::Base>, + Q: &NonIdentityEccPoint, + ) -> Result<(usize, X, Y), Error> { + let config = self.config().clone(); + let mut offset = 0; + + // Assign `x_Q` and `y_Q` in the region and constrain the initial x_a, lambda_1, lambda_2, + // x_p, y_Q using the q_sinsemilla4_private selector. + let y_a: Y = { + // Enable `q_sinsemilla4_private` on the first row. + config.q_sinsemilla4_private.enable(region, offset + 1)?; + let q_y: AssignedCell, pallas::Base> = Q.y().into(); + let y_a: AssignedCell, pallas::Base> = + q_y.copy_advice(|| "fixed y_q", region, config.double_and_add.x_p, offset)?; + + y_a.value_field().into() + }; + offset += 1; + + let x_a: X = { + let q_x: AssignedCell, pallas::Base> = Q.x().into(); + let x_a = q_x.copy_advice(|| "fixed x_q", region, config.double_and_add.x_a, offset)?; + + x_a.into() + }; + + Ok((offset, x_a, y_a)) + } + + #[allow(clippy::type_complexity)] + /// Hash `message` from the initial point `Q`. + /// + /// Before this call to `hash_all_pieces()`, `x_Q` and `y_Q` MUST have been already assigned + /// within this region. + fn hash_all_pieces( + &self, + region: &mut Region<'_, pallas::Base>, + mut offset: usize, + message: &>::Message, + mut x_a: X, + mut y_a: Y, + ) -> Result< + ( + X, + AssignedCell, pallas::Base>, + Vec>>, + ), + Error, + > { + let config = self.config().clone(); + + let mut zs_sum: Vec>> = Vec::new(); + + // Hash each piece in the message. + for (idx, piece) in message.iter().enumerate() { + let final_piece = idx == message.len() - 1; + + // The value of the accumulator after this piece is processed. + let (x, y, zs) = self.hash_piece(region, offset, piece, x_a, y_a, final_piece)?; + + // Since each message word takes one row to process, we increase + // the offset by `piece.num_words` on each iteration. + offset += piece.num_words(); + + // Update the accumulator to the latest value. + x_a = x; + y_a = y; + zs_sum.push(zs); + } + + // Assign the final y_a. + let y_a = { + // Assign the final y_a. + let y_a_cell = + region.assign_advice(|| "y_a", config.double_and_add.lambda_1, offset, || y_a.0)?; + + // Assign lambda_2 and x_p zero values since they are queried + // in the gate. (The actual values do not matter since they are + // multiplied by zero.) + { + region.assign_advice( + || "dummy lambda2", + config.double_and_add.lambda_2, + offset, + || Value::known(pallas::Base::zero()), + )?; + region.assign_advice( + || "dummy x_p", + config.double_and_add.x_p, + offset, + || Value::known(pallas::Base::zero()), + )?; + } + + y_a_cell + }; + + Ok((x_a, y_a, zs_sum)) + } + #[allow(clippy::type_complexity)] /// Hashes a message piece containing `piece.length` number of `K`-bit words. /// diff --git a/halo2_gadgets/src/sinsemilla/merkle/chip.rs b/halo2_gadgets/src/sinsemilla/merkle/chip.rs index 2c37fe92..45f0c5f9 100644 --- a/halo2_gadgets/src/sinsemilla/merkle/chip.rs +++ b/halo2_gadgets/src/sinsemilla/merkle/chip.rs @@ -523,6 +523,19 @@ where chip.hash_to_point(layouter, Q, message) } + #[allow(non_snake_case)] + #[allow(clippy::type_complexity)] + fn hash_to_point_with_private_init( + &self, + layouter: impl Layouter, + Q: &Self::NonIdentityPoint, + message: Self::Message, + ) -> Result<(Self::NonIdentityPoint, Vec>), Error> { + let config = self.config().sinsemilla_config.clone(); + let chip = SinsemillaChip::::construct(config); + chip.hash_to_point_with_private_init(layouter, Q, message) + } + fn extract(point: &Self::NonIdentityPoint) -> Self::X { SinsemillaChip::::extract(point) }