Add functions to evaluate a Sinsemilla hash from an initial private point (#22)

To share ZEC and ZSA hash computations in Orchard circuit's note commitment evaluation, we need to compute a Sinsemille hash from a private input point.
This commit is contained in:
Constance Beguier 2023-10-18 09:46:39 +02:00 committed by Constance
parent f51eebeb4e
commit cba30b1b84
4 changed files with 342 additions and 81 deletions

View File

@ -78,7 +78,7 @@ pub trait SinsemillaInstructions<C: CurveAffine, const K: usize, const MAX_WORDS
/// 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 public point.
#[allow(non_snake_case)]
#[allow(clippy::type_complexity)]
fn hash_to_point(
@ -88,6 +88,20 @@ pub trait SinsemillaInstructions<C: CurveAffine, const K: usize, const MAX_WORDS
message: Self::Message,
) -> Result<(Self::NonIdentityPoint, Vec<Self::RunningSum>), 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<C::Base>,
Q: &Self::NonIdentityPoint,
message: Self::Message,
) -> Result<(Self::NonIdentityPoint, Vec<Self::RunningSum>), 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<C::Base>,
Q: &<SinsemillaChip as SinsemillaInstructions<C, K, MAX_WORDS>>::NonIdentityPoint,
message: Message<C, SinsemillaChip, K, MAX_WORDS>,
) -> Result<(ecc::NonIdentityPoint<C, EccChip>, Vec<SinsemillaChip::RunningSum>), 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<C::Base>,
Q: &<SinsemillaChip as SinsemillaInstructions<C, K, MAX_WORDS>>::NonIdentityPoint,
message: Message<C, SinsemillaChip, K, MAX_WORDS>,
) -> Result<
(
ecc::NonIdentityPoint<C, EccChip>,
Vec<SinsemillaChip::RunningSum>,
),
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].
///

View File

@ -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<Fixed>,
/// 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<pallas::Base>,
Q: &Self::NonIdentityPoint,
message: Self::Message,
) -> Result<(Self::NonIdentityPoint, Vec<Self::RunningSum>), 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()
}

View File

@ -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<pallas::Base> = {
// 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<pallas::Base> = {
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<AssignedCell<pallas::Base, pallas::Base>>> = 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: &<Self as SinsemillaInstructions<
pallas::Affine,
{ sinsemilla::K },
{ sinsemilla::C },
>>::Message,
) -> Result<
(
NonIdentityEccPoint,
Vec<Vec<AssignedCell<pallas::Base, pallas::Base>>>,
),
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<Vec<_>> = 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<bool> = 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<pallas::Base>, Y<pallas::Base>), 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<pallas::Base> = {
// 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<pallas::Base> = {
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<pallas::Base>, Y<pallas::Base>), 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<pallas::Base> = {
// Enable `q_sinsemilla4_private` on the first row.
config.q_sinsemilla4_private.enable(region, offset + 1)?;
let q_y: AssignedCell<Assigned<pallas::Base>, pallas::Base> = Q.y().into();
let y_a: AssignedCell<Assigned<pallas::Base>, 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<pallas::Base> = {
let q_x: AssignedCell<Assigned<pallas::Base>, 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: &<Self as SinsemillaInstructions<
pallas::Affine,
{ sinsemilla::K },
{ sinsemilla::C },
>>::Message,
mut x_a: X<pallas::Base>,
mut y_a: Y<pallas::Base>,
) -> Result<
(
X<pallas::Base>,
AssignedCell<Assigned<pallas::Base>, pallas::Base>,
Vec<Vec<AssignedCell<pallas::Base, pallas::Base>>>,
),
Error,
> {
let config = self.config().clone();
let mut zs_sum: Vec<Vec<AssignedCell<pallas::Base, pallas::Base>>> = 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.
///

View File

@ -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<pallas::Base>,
Q: &Self::NonIdentityPoint,
message: Self::Message,
) -> Result<(Self::NonIdentityPoint, Vec<Vec<Self::CellValue>>), Error> {
let config = self.config().sinsemilla_config.clone();
let chip = SinsemillaChip::<Hash, Commit, F>::construct(config);
chip.hash_to_point_with_private_init(layouter, Q, message)
}
fn extract(point: &Self::NonIdentityPoint) -> Self::X {
SinsemillaChip::<Hash, Commit, F>::extract(point)
}