mirror of https://github.com/zcash/halo2.git
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:
parent
f51eebeb4e
commit
cba30b1b84
|
@ -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].
|
||||
///
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
///
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue