Merge pull request #67 from zcash/sinsemilla-chip-config

Sinsemilla chip with HashDomain
This commit is contained in:
str4d 2021-06-22 16:20:35 +01:00 committed by GitHub
commit 66340e2655
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 1466 additions and 4 deletions

View File

@ -1,3 +1,4 @@
pub(crate) mod ecc;
pub(crate) mod poseidon;
pub(crate) mod sinsemilla;
pub(crate) mod utilities;

View File

@ -27,6 +27,17 @@ pub struct EccPoint {
}
impl EccPoint {
/// Constructs a point from its coordinates, without checking they are on the curve.
///
/// This is an internal API that we only use where we know we have a valid curve point
/// (specifically inside Sinsemilla).
pub(in crate::circuit::gadget) fn from_coordinates_unchecked(
x: CellValue<pallas::Base>,
y: CellValue<pallas::Base>,
) -> Self {
EccPoint { x, y }
}
/// Returns the value of this curve point, if known.
pub fn point(&self) -> Option<pallas::Affine> {
match (self.x.value(), self.y.value()) {

View File

@ -0,0 +1,425 @@
//! Gadget and chips for the Sinsemilla hash function.
use crate::circuit::gadget::{
ecc::{self, EccInstructions},
utilities::Var,
};
use halo2::{arithmetic::CurveAffine, circuit::Layouter, plonk::Error};
use std::fmt::Debug;
pub mod chip;
mod message;
/// The set of circuit instructions required to use the [`Sinsemilla`](https://zcash.github.io/halo2/design/gadgets/sinsemilla.html) gadget.
/// This trait is bounded on two constant parameters: `K`, the number of bits
/// in each word accepted by the Sinsemilla hash, and `MAX_WORDS`, the maximum
/// number of words that a single hash instance can process.
pub trait SinsemillaInstructions<C: CurveAffine, const K: usize, const MAX_WORDS: usize> {
/// A variable in the circuit.
type CellValue: Var<C::Base>;
/// A message composed of [`Self::MessagePiece`]s.
type Message: From<Vec<Self::MessagePiece>>;
/// A piece in a message containing a number of `K`-bit words.
/// A [`Self::MessagePiece`] fits in a single base field element,
/// which means it can only contain up to `N` words, where
/// `N*K <= C::Base::NUM_BITS`.
///
/// For example, in the case `K = 10`, `NUM_BITS = 255`, we can fit
/// up to `N = 25` words in a single base field element.
type MessagePiece;
/// The x-coordinate of a point output of [`Self::hash_to_point`].
type X;
/// A point output of [`Self::hash_to_point`].
type Point: Clone + Debug;
/// HashDomains used in this instruction.
type HashDomains: HashDomains<C>;
/// Witness a message in the given bitstring.
/// Returns a vector of [`Self::MessagePiece`]s encoding the given message.
///
/// # Panics
///
/// Panics if the message length is not a multiple of `K`.
///
/// Panics if the message length exceeds `K * MAX_WORDS`.
fn witness_message(
&self,
layouter: impl Layouter<C::Base>,
message: Vec<Option<bool>>,
) -> Result<Self::Message, Error>;
/// Witnesses a message piece given a field element and the intended number of `K`-bit
/// words it contains.
///
/// Returns a [`Self::MessagePiece`] encoding the given message.
///
/// # Panics
///
/// Panics if the message length is not a multiple of `K`.
///
/// Panics if the message length exceeds the maximum number of words
/// that can fit in a field element.
fn witness_message_piece_bitstring(
&self,
layouter: impl Layouter<C::Base>,
message: &[Option<bool>],
) -> Result<Self::MessagePiece, Error>;
/// Witness a message piece given a field element. Returns a [`Self::MessagePiece`]
/// encoding the given message.
///
/// # Panics
///
/// Panics if `num_words` exceed the maximum number of `K`-bit words that
/// can fit into a single base field element.
fn witness_message_piece_field(
&self,
layouter: impl Layouter<C::Base>,
value: Option<C::Base>,
num_words: usize,
) -> Result<Self::MessagePiece, 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.
///
/// A cumulative sum `z` is used to decompose a Sinsemilla message. It
/// produces intermediate values for each word in the message, such
/// that `z_next` = (`z_cur` - `word_next`) / `2^K`.
///
/// These intermediate values are useful for range checks on subsets
/// of the Sinsemilla message. Sinsemilla messages in the Orchard
/// protocol are composed of field elements, and we need to check
/// the canonicity of the field element encodings in certain cases.
///
#[allow(non_snake_case)]
#[allow(clippy::type_complexity)]
fn hash_to_point(
&self,
layouter: impl Layouter<C::Base>,
Q: C,
message: Self::Message,
) -> Result<(Self::Point, Vec<Vec<Self::CellValue>>), Error>;
/// Extracts the x-coordinate of the output of a Sinsemilla hash.
fn extract(point: &Self::Point) -> Self::X;
}
/// A message to be hashed.
///
/// Composed of [`MessagePiece`]s with bitlength some multiple of `K`.
///
/// [`MessagePiece`]: SinsemillaInstructions::MessagePiece
#[derive(Clone, Debug)]
pub struct Message<C: CurveAffine, SinsemillaChip, const K: usize, const MAX_WORDS: usize>
where
SinsemillaChip: SinsemillaInstructions<C, K, MAX_WORDS> + Clone + Debug + Eq,
{
chip: SinsemillaChip,
inner: SinsemillaChip::Message,
}
impl<C: CurveAffine, SinsemillaChip, const K: usize, const MAX_WORDS: usize>
Message<C, SinsemillaChip, K, MAX_WORDS>
where
SinsemillaChip: SinsemillaInstructions<C, K, MAX_WORDS> + Clone + Debug + Eq,
{
fn from_bitstring(
chip: SinsemillaChip,
layouter: impl Layouter<C::Base>,
bitstring: Vec<Option<bool>>,
) -> Result<Self, Error> {
let inner = chip.witness_message(layouter, bitstring)?;
Ok(Self { chip, inner })
}
/// Constructs a message from a vector of [`MessagePiece`]s.
///
/// [`MessagePiece`]: SinsemillaInstructions::MessagePiece
fn from_pieces(chip: SinsemillaChip, pieces: Vec<SinsemillaChip::MessagePiece>) -> Self {
Self {
chip,
inner: pieces.into(),
}
}
}
/// A domain in which $\mathsf{SinsemillaHashToPoint}$ and $\mathsf{SinsemillaHash}$ can
/// be used.
#[allow(non_snake_case)]
pub struct HashDomain<
C: CurveAffine,
SinsemillaChip,
EccChip,
const K: usize,
const MAX_WORDS: usize,
> where
SinsemillaChip: SinsemillaInstructions<C, K, MAX_WORDS> + Clone + Debug + Eq,
EccChip: EccInstructions<
C,
Point = <SinsemillaChip as SinsemillaInstructions<C, K, MAX_WORDS>>::Point,
> + Clone
+ Debug
+ Eq,
{
sinsemilla_chip: SinsemillaChip,
ecc_chip: EccChip,
Q: C,
}
impl<C: CurveAffine, SinsemillaChip, EccChip, const K: usize, const MAX_WORDS: usize>
HashDomain<C, SinsemillaChip, EccChip, K, MAX_WORDS>
where
SinsemillaChip: SinsemillaInstructions<C, K, MAX_WORDS> + Clone + Debug + Eq,
EccChip: EccInstructions<
C,
Point = <SinsemillaChip as SinsemillaInstructions<C, K, MAX_WORDS>>::Point,
> + Clone
+ Debug
+ Eq,
{
#[allow(non_snake_case)]
/// Constructs a new `HashDomain` for the given domain.
pub fn new(
sinsemilla_chip: SinsemillaChip,
ecc_chip: EccChip,
domain: &SinsemillaChip::HashDomains,
) -> Self {
HashDomain {
sinsemilla_chip,
ecc_chip,
Q: domain.Q(),
}
}
/// $\mathsf{SinsemillaHashToPoint}$ from [§ 5.4.1.9][concretesinsemillahash].
///
/// [concretesinsemillahash]: https://zips.z.cash/protocol/protocol.pdf#concretesinsemillahash
pub fn hash_to_point(
&self,
layouter: impl Layouter<C::Base>,
message: Message<C, SinsemillaChip, K, MAX_WORDS>,
) -> Result<ecc::Point<C, EccChip>, Error> {
assert_eq!(self.sinsemilla_chip, message.chip);
self.sinsemilla_chip
.hash_to_point(layouter, self.Q, message.inner)
.map(|(point, _)| ecc::Point::from_inner(self.ecc_chip.clone(), point))
}
/// $\mathsf{SinsemillaHash}$ from [§ 5.4.1.9][concretesinsemillahash].
///
/// [concretesinsemillahash]: https://zips.z.cash/protocol/protocol.pdf#concretesinsemillahash
pub fn hash(
&self,
layouter: impl Layouter<C::Base>,
message: Message<C, SinsemillaChip, K, MAX_WORDS>,
) -> Result<ecc::X<C, EccChip>, Error> {
assert_eq!(self.sinsemilla_chip, message.chip);
let p = self.hash_to_point(layouter, message);
p.map(|p| p.extract_p())
}
}
/// Trait allowing circuit's Sinsemilla HashDomains to be enumerated.
#[allow(non_snake_case)]
pub trait HashDomains<C: CurveAffine>: Clone + Debug {
fn Q(&self) -> C;
}
#[cfg(test)]
mod tests {
use halo2::{
circuit::{layouter::SingleChipLayouter, Layouter},
dev::MockProver,
pasta::pallas,
plonk::{Assignment, Circuit, ConstraintSystem, Error},
};
use super::{
chip::SinsemillaHashDomains,
chip::{SinsemillaChip, SinsemillaConfig},
HashDomain, Message, SinsemillaInstructions,
};
use crate::{
circuit::gadget::ecc::{
chip::{EccChip, EccConfig},
Point,
},
constants::MERKLE_CRH_PERSONALIZATION,
primitives::sinsemilla::{self, K},
};
use group::Curve;
use std::convert::TryInto;
struct MyCircuit {}
impl Circuit<pallas::Base> for MyCircuit {
type Config = (EccConfig, SinsemillaConfig, SinsemillaConfig);
#[allow(non_snake_case)]
fn configure(meta: &mut ConstraintSystem<pallas::Base>) -> 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 constants = meta.fixed_column();
let perm = meta.permutation(
&advices
.iter()
.map(|advice| (*advice).into())
.chain(Some(constants.into()))
.collect::<Vec<_>>(),
);
let ecc_config = EccChip::configure(meta, advices, perm.clone());
// Fixed columns for the Sinsemilla generator lookup table
let lookup = (
meta.fixed_column(),
meta.fixed_column(),
meta.fixed_column(),
);
let config1 = SinsemillaChip::configure(
meta,
advices[..5].try_into().unwrap(),
lookup,
constants,
perm.clone(),
);
let config2 = SinsemillaChip::configure(
meta,
advices[5..].try_into().unwrap(),
lookup,
constants,
perm,
);
(ecc_config, config1, config2)
}
fn synthesize(
&self,
cs: &mut impl Assignment<pallas::Base>,
config: Self::Config,
) -> Result<(), Error> {
let mut layouter = SingleChipLayouter::new(cs)?;
let ecc_chip = EccChip::construct(config.0);
// The two `SinsemillaChip`s share the same lookup table.
SinsemillaChip::load(config.1.clone(), &mut layouter)?;
// This MerkleCRH example is purely for illustrative purposes.
// It is not an implementation of the Orchard protocol spec.
{
let chip1 = SinsemillaChip::construct(config.1);
let merkle_crh = HashDomain::new(
chip1.clone(),
ecc_chip.clone(),
&SinsemillaHashDomains::MerkleCrh,
);
// Layer 31, l = MERKLE_DEPTH_ORCHARD - 1 - layer = 0
let l_bitstring = vec![Some(false); K];
let l = chip1
.witness_message_piece_bitstring(layouter.namespace(|| "l"), &l_bitstring)?;
// Left leaf
let left_bitstring: Vec<Option<bool>> =
(0..250).map(|_| Some(rand::random::<bool>())).collect();
let left = chip1.witness_message_piece_bitstring(
layouter.namespace(|| "left"),
&left_bitstring,
)?;
// Right leaf
let right_bitstring: Vec<Option<bool>> =
(0..250).map(|_| Some(rand::random::<bool>())).collect();
let right = chip1.witness_message_piece_bitstring(
layouter.namespace(|| "right"),
&right_bitstring,
)?;
let l_bitstring: Option<Vec<bool>> = l_bitstring.into_iter().collect();
let left_bitstring: Option<Vec<bool>> = left_bitstring.into_iter().collect();
let right_bitstring: Option<Vec<bool>> = right_bitstring.into_iter().collect();
// Witness expected parent
let expected_parent = {
let expected_parent = if let (Some(l), Some(left), Some(right)) =
(l_bitstring, left_bitstring, right_bitstring)
{
let merkle_crh = sinsemilla::HashDomain::new(MERKLE_CRH_PERSONALIZATION);
let point = merkle_crh
.hash_to_point(
l.into_iter()
.chain(left.into_iter())
.chain(right.into_iter()),
)
.unwrap();
Some(point.to_affine())
} else {
None
};
Point::new(
ecc_chip,
layouter.namespace(|| "Witness expected parent"),
expected_parent,
)?
};
// Parent
let parent = {
let message = Message::from_pieces(chip1, vec![l, left, right]);
merkle_crh.hash_to_point(layouter.namespace(|| "parent"), message)?
};
parent.constrain_equal(
layouter.namespace(|| "parent == expected parent"),
&expected_parent,
)?;
}
Ok(())
}
}
#[test]
fn sinsemilla_chip() {
let k = 11;
let circuit = MyCircuit {};
let prover = MockProver::run(k, &circuit, vec![]).unwrap();
assert_eq!(prover.verify(), Ok(()))
}
#[cfg(feature = "dev-graph")]
#[test]
fn print_sinsemilla_chip() {
use plotters::prelude::*;
let root =
BitMapBackend::new("sinsemilla-hash-layout.png", (1024, 7680)).into_drawing_area();
root.fill(&WHITE).unwrap();
let root = root.titled("SinsemillaHash", ("sans-serif", 60)).unwrap();
let circuit = MyCircuit {};
halo2::dev::circuit_layout(&circuit, &root).unwrap();
}
}

View File

@ -0,0 +1,361 @@
use super::{
message::{Message, MessagePiece},
HashDomains, SinsemillaInstructions,
};
use crate::{
circuit::gadget::{
ecc::chip::EccPoint,
utilities::{CellValue, Var},
},
primitives::sinsemilla::{
self, Q_COMMIT_IVK_M_GENERATOR, Q_MERKLE_CRH, Q_NOTE_COMMITMENT_M_GENERATOR,
},
};
use ff::PrimeField;
use halo2::{
arithmetic::{CurveAffine, FieldExt},
circuit::{Chip, Layouter},
plonk::{
Advice, Column, ConstraintSystem, Error, Expression, Fixed, Permutation, Selector,
VirtualCells,
},
poly::Rotation,
};
use pasta_curves::pallas;
use std::convert::TryInto;
mod generator_table;
pub use generator_table::get_s_by_idx;
use generator_table::GeneratorTableConfig;
mod hash_to_point;
/// Configuration for the Sinsemilla hash chip
#[derive(Eq, PartialEq, Clone, Debug)]
pub struct SinsemillaConfig {
/// Selector used in the lookup argument as well as Sinsemilla custom gates.
q_sinsemilla1: Selector,
/// Fixed column used in Sinsemilla custom gates, to toggle behaviour at the ends of
/// message pieces.
q_sinsemilla2: Column<Fixed>,
/// Fixed column used to constrain hash initialization to be consistent with
/// the y-coordinate of the domain $Q$.
fixed_y_q: Column<Fixed>,
/// Advice column used to store the x-coordinate of the accumulator at each
/// iteration of the hash.
x_a: Column<Advice>,
/// Advice column used to store the x-coordinate of the generator corresponding
/// to the message word at each iteration of the hash. This is looked up in the
/// generator table.
x_p: Column<Advice>,
/// Advice column used to load the message.
bits: Column<Advice>,
/// Advice column used to store the $\lambda_1$ intermediate value at each
/// iteration.
lambda_1: Column<Advice>,
/// Advice column used to store the $\lambda_2$ intermediate value at each
/// iteration.
lambda_2: Column<Advice>,
/// The lookup table where $(\mathsf{idx}, x_p, y_p)$ are loaded for the $2^K$
/// generators of the Sinsemilla hash.
generator_table: GeneratorTableConfig,
/// Fixed column shared by the whole circuit. This is used to load the
/// x-coordinate of the domain $Q$, which is then constrained to equal the
/// initial $x_a$.
constants: Column<Fixed>,
/// Permutation over all advice columns and the `constants` fixed column.
perm: Permutation,
}
#[derive(Eq, PartialEq, Clone, Debug)]
pub struct SinsemillaChip {
config: SinsemillaConfig,
}
impl Chip<pallas::Base> for SinsemillaChip {
type Config = SinsemillaConfig;
type Loaded = ();
fn config(&self) -> &Self::Config {
&self.config
}
fn loaded(&self) -> &Self::Loaded {
&()
}
}
impl SinsemillaChip {
pub fn construct(config: <Self as Chip<pallas::Base>>::Config) -> Self {
Self { config }
}
pub fn load(
config: SinsemillaConfig,
layouter: &mut impl Layouter<pallas::Base>,
) -> Result<<Self as Chip<pallas::Base>>::Loaded, Error> {
// Load the lookup table.
config.generator_table.load(layouter)
}
#[allow(clippy::too_many_arguments)]
#[allow(non_snake_case)]
pub fn configure(
meta: &mut ConstraintSystem<pallas::Base>,
advices: [Column<Advice>; 5],
lookup: (Column<Fixed>, Column<Fixed>, Column<Fixed>),
constants: Column<Fixed>,
perm: Permutation,
) -> <Self as Chip<pallas::Base>>::Config {
let config = SinsemillaConfig {
q_sinsemilla1: meta.selector(),
q_sinsemilla2: meta.fixed_column(),
fixed_y_q: meta.fixed_column(),
x_a: advices[0],
x_p: advices[1],
bits: advices[2],
lambda_1: advices[3],
lambda_2: advices[4],
generator_table: GeneratorTableConfig {
table_idx: lookup.0,
table_x: lookup.1,
table_y: lookup.2,
},
constants,
perm,
};
// Set up lookup argument
GeneratorTableConfig::configure(meta, config.clone());
// Constant expressions
let two = Expression::Constant(pallas::Base::from_u64(2));
// Closures for expressions that are derived multiple times
// x_r = lambda_1^2 - x_a - x_p
let x_r = |meta: &mut VirtualCells<pallas::Base>, rotation| {
let x_a = meta.query_advice(config.x_a, rotation);
let x_p = meta.query_advice(config.x_p, rotation);
let lambda_1 = meta.query_advice(config.lambda_1, rotation);
lambda_1.square() - x_a - x_p
};
// Y_A = (lambda_1 + lambda_2) * (x_a - x_r)
let Y_A = |meta: &mut VirtualCells<pallas::Base>, rotation| {
let x_a = meta.query_advice(config.x_a, rotation);
let lambda_1 = meta.query_advice(config.lambda_1, rotation);
let lambda_2 = meta.query_advice(config.lambda_2, rotation);
(lambda_1 + lambda_2) * (x_a - x_r(meta, rotation))
};
meta.create_gate("Sinsemilla gate", |meta| {
let q_s1 = meta.query_selector(config.q_sinsemilla1);
let q_s2 = meta.query_fixed(config.q_sinsemilla2, Rotation::cur());
let q_s3 = {
let one = Expression::Constant(pallas::Base::one());
q_s2.clone() * (q_s2 - one)
};
let fixed_y_q = meta.query_fixed(config.fixed_y_q, Rotation::cur());
let lambda_1_next = meta.query_advice(config.lambda_1, Rotation::next());
let lambda_2_cur = meta.query_advice(config.lambda_2, Rotation::cur());
let x_a_cur = meta.query_advice(config.x_a, Rotation::cur());
let x_a_next = meta.query_advice(config.x_a, Rotation::next());
// x_r = lambda_1^2 - x_a_cur - x_p
let x_r = x_r(meta, Rotation::cur());
// Y_A = (lambda_1 + lambda_2) * (x_a - x_r)
let Y_A_cur = Y_A(meta, Rotation::cur());
// Y_A = (lambda_1 + lambda_2) * (x_a - x_r)
let Y_A_next = Y_A(meta, Rotation::next());
// Check that the initial x_A, x_P, lambda_1, lambda_2 are consistent with y_Q.
// fixed_y_q * (2 * fixed_y_q - Y_{A,0}) = 0
let init_y_q_check = fixed_y_q.clone() * (two.clone() * fixed_y_q - Y_A_cur.clone());
// lambda2^2 - (x_a_next + x_r + x_a_cur) = 0
let secant_line =
lambda_2_cur.clone().square() - (x_a_next.clone() + x_r + x_a_cur.clone());
// lhs - rhs = 0, where
// - lhs = 4 * lambda_2_cur * (x_a_cur - x_a_next)
// - rhs = (2 * Y_A_cur + (2 - q_s3) * Y_A_next + 2 * q_s3 * y_a_final)
let y_check = {
// lhs = 4 * lambda_2_cur * (x_a_cur - x_a_next)
let lhs = lambda_2_cur * pallas::Base::from_u64(4) * (x_a_cur - x_a_next);
// rhs = 2 * Y_A_cur + (2 - q_s3) * Y_A_next + 2 * q_s3 * y_a_final
let rhs = {
// y_a_final is assigned to the lambda1 column on the next offset.
let y_a_final = lambda_1_next;
two.clone() * Y_A_cur
+ (two.clone() - q_s3.clone()) * Y_A_next
+ two * q_s3 * y_a_final
};
lhs - rhs
};
vec![
("Initial y_q", init_y_q_check),
("Secant line", q_s1.clone() * secant_line),
("y check", q_s1 * y_check),
]
});
config
}
}
// Implement `SinsemillaInstructions` for `SinsemillaChip`
impl SinsemillaInstructions<pallas::Affine, { sinsemilla::K }, { sinsemilla::C }>
for SinsemillaChip
{
type CellValue = CellValue<pallas::Base>;
type Message = Message<pallas::Base, { sinsemilla::K }, { sinsemilla::C }>;
type MessagePiece = MessagePiece<pallas::Base, { sinsemilla::K }>;
type X = CellValue<pallas::Base>;
type Point = EccPoint;
type HashDomains = SinsemillaHashDomains;
#[allow(non_snake_case)]
fn witness_message(
&self,
mut layouter: impl Layouter<pallas::Base>,
message: Vec<Option<bool>>,
) -> Result<Self::Message, Error> {
// Message must be composed of `K`-bit words.
assert_eq!(message.len() % sinsemilla::K, 0);
// Message must have at most `sinsemilla::C` words.
assert!(message.len() / sinsemilla::K <= sinsemilla::C);
// Message piece must be at most `ceil(pallas::Base::NUM_BITS / sinsemilla::K)` bits
let piece_num_words = pallas::Base::NUM_BITS as usize / sinsemilla::K;
let pieces: Result<Vec<_>, _> = message
.chunks(piece_num_words * sinsemilla::K)
.enumerate()
.map(|(i, piece)| -> Result<Self::MessagePiece, Error> {
self.witness_message_piece_bitstring(
layouter.namespace(|| format!("message piece {}", i)),
piece,
)
})
.collect();
pieces.map(|pieces| pieces.into())
}
#[allow(non_snake_case)]
fn witness_message_piece_bitstring(
&self,
layouter: impl Layouter<pallas::Base>,
message_piece: &[Option<bool>],
) -> Result<Self::MessagePiece, Error> {
// Message must be composed of `K`-bit words.
assert_eq!(message_piece.len() % sinsemilla::K, 0);
let num_words = message_piece.len() / sinsemilla::K;
// Message piece must be at most `ceil(C::Base::NUM_BITS / sinsemilla::K)` bits
let piece_max_num_words = pallas::Base::NUM_BITS as usize / sinsemilla::K;
assert!(num_words <= piece_max_num_words as usize);
// Closure to parse a bitstring (little-endian) into a base field element.
let to_base_field = |bits: &[Option<bool>]| -> Option<pallas::Base> {
assert!(bits.len() <= pallas::Base::NUM_BITS as usize);
let bits: Option<Vec<bool>> = bits.iter().cloned().collect();
let bytes: Option<Vec<u8>> = bits.map(|bits| {
// Pad bits to 256 bits
let pad_len = 256 - bits.len();
let mut bits = bits;
bits.extend_from_slice(&vec![false; pad_len]);
bits.chunks_exact(8)
.map(|byte| byte.iter().rev().fold(0u8, |acc, bit| acc * 2 + *bit as u8))
.collect()
});
bytes.map(|bytes| pallas::Base::from_bytes(&bytes.try_into().unwrap()).unwrap())
};
let piece_value = to_base_field(message_piece);
self.witness_message_piece_field(layouter, piece_value, num_words)
}
fn witness_message_piece_field(
&self,
mut layouter: impl Layouter<pallas::Base>,
field_elem: Option<pallas::Base>,
num_words: usize,
) -> Result<Self::MessagePiece, Error> {
let config = self.config().clone();
let cell = layouter.assign_region(
|| "witness message piece",
|mut region| {
region.assign_advice(
|| "witness message piece",
config.bits,
0,
|| field_elem.ok_or(Error::SynthesisError),
)
},
)?;
Ok(MessagePiece::new(cell, field_elem, num_words))
}
#[allow(non_snake_case)]
#[allow(clippy::type_complexity)]
fn hash_to_point(
&self,
mut layouter: impl Layouter<pallas::Base>,
Q: pallas::Affine,
message: Self::Message,
) -> Result<(Self::Point, Vec<Vec<Self::CellValue>>), Error> {
layouter.assign_region(
|| "hash_to_point",
|mut region| self.hash_message(&mut region, Q, &message),
)
}
fn extract(point: &Self::Point) -> Self::X {
point.x()
}
}
#[derive(Clone, Debug)]
pub enum SinsemillaHashDomains {
NoteCommit,
CommitIvk,
MerkleCrh,
}
#[allow(non_snake_case)]
impl HashDomains<pallas::Affine> for SinsemillaHashDomains {
fn Q(&self) -> pallas::Affine {
match self {
SinsemillaHashDomains::CommitIvk => pallas::Affine::from_xy(
pallas::Base::from_bytes(&Q_COMMIT_IVK_M_GENERATOR.0).unwrap(),
pallas::Base::from_bytes(&Q_COMMIT_IVK_M_GENERATOR.1).unwrap(),
)
.unwrap(),
SinsemillaHashDomains::NoteCommit => pallas::Affine::from_xy(
pallas::Base::from_bytes(&Q_NOTE_COMMITMENT_M_GENERATOR.0).unwrap(),
pallas::Base::from_bytes(&Q_NOTE_COMMITMENT_M_GENERATOR.1).unwrap(),
)
.unwrap(),
SinsemillaHashDomains::MerkleCrh => pallas::Affine::from_xy(
pallas::Base::from_bytes(&Q_MERKLE_CRH.0).unwrap(),
pallas::Base::from_bytes(&Q_MERKLE_CRH.1).unwrap(),
)
.unwrap(),
}
}
}

View File

@ -0,0 +1,131 @@
use crate::primitives::sinsemilla::{self, sinsemilla_s_generators, S_PERSONALIZATION};
use halo2::{
circuit::Layouter,
plonk::{Column, ConstraintSystem, Error, Expression, Fixed},
poly::Rotation,
};
use pasta_curves::{
arithmetic::{CurveAffine, CurveExt, FieldExt},
pallas,
};
use group::Curve;
/// Table containing independent generators S[0..2^k]
#[derive(Eq, PartialEq, Copy, Clone, Debug)]
pub struct GeneratorTableConfig {
pub table_idx: Column<Fixed>,
pub table_x: Column<Fixed>,
pub table_y: Column<Fixed>,
}
impl GeneratorTableConfig {
#[allow(clippy::too_many_arguments)]
#[allow(non_snake_case)]
/// Even though the lookup table can be used in other parts of the circuit,
/// this specific configuration sets up Sinsemilla-specific constraints
/// controlled by `q_sinsemilla`, and would likely not apply to other chips.
pub fn configure(meta: &mut ConstraintSystem<pallas::Base>, config: super::SinsemillaConfig) {
let (table_idx, table_x, table_y) = (
config.generator_table.table_idx,
config.generator_table.table_x,
config.generator_table.table_y,
);
meta.lookup(|meta| {
let q_s1 = meta.query_selector(config.q_sinsemilla1);
let q_s2 = meta.query_fixed(config.q_sinsemilla2, Rotation::cur());
let q_s3 = {
let one = Expression::Constant(pallas::Base::one());
q_s2.clone() * (q_s2.clone() - one)
};
let table_idx_cur = meta.query_fixed(table_idx, Rotation::cur());
let table_x_cur = meta.query_fixed(table_x, Rotation::cur());
let table_y_cur = meta.query_fixed(table_y, Rotation::cur());
// m_{i+1} = z_{i} - 2^K * (q_s2 - q_s3) * z_{i + 1}
// Note that the message words m_i's are 1-indexed while the
// running sum z_i's are 0-indexed.
let word = {
let z_cur = meta.query_advice(config.bits, Rotation::cur());
let z_next = meta.query_advice(config.bits, Rotation::next());
z_cur - ((q_s2 - q_s3) * z_next * pallas::Base::from_u64(1 << sinsemilla::K))
};
let x_p = meta.query_advice(config.x_p, Rotation::cur());
// y_{p,i} = (Y_{A,i} / 2) - lambda1 * (x_{A,i} - x_{P,i}),
// where Y_{A,i} = (lambda1_i + lambda2_i) * (x_{A,i} - x_{R,i}),
// x_{R,i} = lambda1^2 - x_{A,i} - x_{P,i}
//
let y_p = {
let lambda1 = meta.query_advice(config.lambda_1, Rotation::cur());
let lambda2 = meta.query_advice(config.lambda_2, Rotation::cur());
let x_a = meta.query_advice(config.x_a, Rotation::cur());
let x_r = lambda1.clone().square() - x_a.clone() - x_p.clone();
let Y_A = (lambda1.clone() + lambda2) * (x_a.clone() - x_r);
(Y_A * pallas::Base::TWO_INV) - (lambda1 * (x_a - x_p.clone()))
};
// Lookup expressions default to the first entry when `q_s1`
// is not enabled.
let (init_x, init_y) = {
let init_p = get_s_by_idx(0).to_affine().coordinates().unwrap();
(*init_p.x(), *init_p.y())
};
let not_q_s1 = Expression::Constant(pallas::Base::one()) - q_s1.clone();
let m = q_s1.clone() * word; // The first table index is 0.
let x_p = q_s1.clone() * x_p + not_q_s1.clone() * init_x;
let y_p = q_s1 * y_p + not_q_s1 * init_y;
vec![(m, table_idx_cur), (x_p, table_x_cur), (y_p, table_y_cur)]
});
}
pub fn load(&self, layouter: &mut impl Layouter<pallas::Base>) -> Result<(), Error> {
layouter.assign_region(
|| "generator_table",
|mut gate| {
// We generate the row values lazily (we only need them during keygen).
let mut rows = sinsemilla_s_generators();
for index in 0..(1 << sinsemilla::K) {
let mut row = None;
gate.assign_fixed(
|| "table_idx",
self.table_idx,
index,
|| {
row = rows.next();
Ok(pallas::Base::from_u64(index as u64))
},
)?;
gate.assign_fixed(
|| "table_x",
self.table_x,
index,
|| row.map(|(x, _)| x).ok_or(Error::SynthesisError),
)?;
gate.assign_fixed(
|| "table_y",
self.table_y,
index,
|| row.map(|(_, y)| y).ok_or(Error::SynthesisError),
)?;
}
Ok(())
},
)
}
}
/// Get generator S by index
pub fn get_s_by_idx(idx: u32) -> pallas::Point {
let hash = pallas::Point::hash_to_curve(S_PERSONALIZATION);
hash(&idx.to_le_bytes())
}

View File

@ -0,0 +1,461 @@
use super::super::SinsemillaInstructions;
use super::{get_s_by_idx, CellValue, EccPoint, SinsemillaChip, Var};
use crate::{
circuit::gadget::utilities::copy,
primitives::sinsemilla::{self, lebs2ip_k, INV_TWO_POW_K},
};
use halo2::{
circuit::{Chip, Region},
plonk::Error,
};
use ff::{Field, PrimeFieldBits};
use group::Curve;
use pasta_curves::{
arithmetic::{CurveAffine, FieldExt},
pallas,
};
use std::ops::Deref;
impl SinsemillaChip {
#[allow(non_snake_case)]
#[allow(clippy::type_complexity)]
pub(super) fn hash_message(
&self,
region: &mut Region<'_, pallas::Base>,
Q: pallas::Affine,
message: &<Self as SinsemillaInstructions<
pallas::Affine,
{ sinsemilla::K },
{ sinsemilla::C },
>>::Message,
) -> Result<(EccPoint, Vec<Vec<CellValue<pallas::Base>>>), Error> {
let config = self.config().clone();
let mut 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();
// Initialize the accumulator to `Q`.
let (mut x_a, mut y_a): (X<pallas::Base>, Y<pallas::Base>) = {
// Constrain the initial x_q to equal the x-coordinate of the domain's `Q`.
let fixed_x_q = {
let cell =
region.assign_fixed(|| "fixed x_q", config.constants, offset, || Ok(x_q))?;
CellValue::new(cell, Some(x_q))
};
let x_a = copy(
region,
|| "x_q",
config.x_a,
offset,
&fixed_x_q,
&config.perm,
)?;
// Constrain the initial x_a, lambda_1, lambda_2, x_p using the fixed y_q
// initializer. Assign `fixed_y_q` to be zero on every other row.
{
region.assign_fixed(|| "fixed y_q", config.fixed_y_q, offset, || Ok(y_q))?;
let total_num_words = message.iter().map(|piece| piece.num_words()).sum();
for row in 1..total_num_words {
region.assign_fixed(
|| "fixed y_q",
config.fixed_y_q,
offset + row,
|| Ok(pallas::Base::zero()),
)?;
}
}
let y_a = Some(y_q);
(x_a.into(), y_a.into())
};
let mut zs_sum: Vec<Vec<CellValue<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.lambda_1,
offset,
|| y_a.ok_or(Error::SynthesisError),
)?;
// 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.lambda_2,
offset,
|| Ok(pallas::Base::zero()),
)?;
region.assign_advice(
|| "dummy x_p",
config.x_p,
offset,
|| Ok(pallas::Base::zero()),
)?;
}
CellValue::new(y_a_cell, y_a.0)
};
#[cfg(test)]
#[allow(non_snake_case)]
// Check equivalence to result from primitives::sinsemilla::hash_to_point
{
use crate::circuit::gadget::sinsemilla::message::MessagePiece;
use crate::primitives::sinsemilla::{K, S_PERSONALIZATION};
use group::prime::PrimeCurveAffine;
use pasta_curves::arithmetic::CurveExt;
let field_elems: Option<Vec<pallas::Base>> =
message.iter().map(|piece| piece.field_elem()).collect();
if field_elems.is_some() {
// Get message as a bitstring.
let bitstring: Vec<bool> = message
.iter()
.map(|piece: &MessagePiece<pallas::Base, K>| {
piece
.field_elem()
.unwrap()
.to_le_bits()
.into_iter()
.take(K * piece.num_words())
.collect::<Vec<_>>()
})
.flatten()
.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.value().unwrap(), y_a.value().unwrap()).unwrap();
assert_eq!(expected_point.to_affine(), actual_point);
}
}
Ok((EccPoint::from_coordinates_unchecked(x_a.0, y_a), zs_sum))
}
#[allow(clippy::type_complexity)]
/// Hashes a message piece containing `piece.length` number of `K`-bit words.
///
/// To avoid a duplicate assignment, the accumulator x-coordinate provided
/// by the caller is not copied. This only works because `hash_piece()` is
/// an internal API. Before this call to `hash_piece()`, x_a MUST have been
/// already assigned within this region at the correct offset.
fn hash_piece(
&self,
region: &mut Region<'_, pallas::Base>,
offset: usize,
piece: &<Self as SinsemillaInstructions<
pallas::Affine,
{ sinsemilla::K },
{ sinsemilla::C },
>>::MessagePiece,
mut x_a: X<pallas::Base>,
mut y_a: Y<pallas::Base>,
final_piece: bool,
) -> Result<
(
X<pallas::Base>,
Y<pallas::Base>,
Vec<CellValue<pallas::Base>>,
),
Error,
> {
let config = self.config().clone();
// Selector assignments
{
// Enable `q_sinsemilla1` selector on every row.
for row in 0..piece.num_words() {
config.q_sinsemilla1.enable(region, offset + row)?;
}
// Set `q_sinsemilla2` fixed column to 1 on every row but the last.
for row in 0..(piece.num_words() - 1) {
region.assign_fixed(
|| "q_s2 = 1",
config.q_sinsemilla2,
offset + row,
|| Ok(pallas::Base::one()),
)?;
}
// Set `q_sinsemilla2` fixed column to 0 on the last row if this is
// not the final piece, or to 2 on the last row of the final piece.
region.assign_fixed(
|| {
if final_piece {
"q_s2 for final piece"
} else {
"q_s2 between pieces"
}
},
config.q_sinsemilla2,
offset + piece.num_words() - 1,
|| {
Ok(if final_piece {
pallas::Base::from_u64(2)
} else {
pallas::Base::zero()
})
},
)?;
}
// Message piece as K * piece.length bitstring
let bitstring: Option<Vec<bool>> = piece.field_elem().map(|value| {
value
.to_le_bits()
.into_iter()
.take(sinsemilla::K * piece.num_words())
.collect()
});
let words: Option<Vec<u32>> = bitstring.map(|bitstring| {
bitstring
.chunks_exact(sinsemilla::K)
.map(|word| lebs2ip_k(word))
.collect()
});
// Get (x_p, y_p) for each word. We precompute this here so that we can use `batch_normalize()`.
let generators_projective: Option<Vec<pallas::Point>> = words
.clone()
.map(|words| words.iter().map(|word| get_s_by_idx(*word)).collect());
let generators: Option<Vec<(pallas::Base, pallas::Base)>> =
generators_projective.map(|generators_projective| {
let mut generators = vec![pallas::Affine::default(); generators_projective.len()];
pallas::Point::batch_normalize(&generators_projective, &mut generators);
generators
.iter()
.map(|gen| {
let point = gen.coordinates().unwrap();
(*point.x(), *point.y())
})
.collect()
});
// Convert `words` from `Option<Vec<u32>>` to `Vec<Option<u32>>`
let words: Vec<Option<u32>> = if let Some(words) = words {
words.into_iter().map(Some).collect()
} else {
vec![None; piece.num_words()]
};
// Decompose message piece into `K`-bit pieces with a running sum `z`.
let zs = {
let mut zs = Vec::with_capacity(piece.num_words() + 1);
// Copy message and initialize running sum `z` to decompose message in-circuit
let cell = region.assign_advice(
|| "z_0 (copy of message piece)",
config.bits,
offset,
|| piece.field_elem().ok_or(Error::SynthesisError),
)?;
region.constrain_equal(&config.perm, piece.cell(), cell)?;
zs.push(CellValue::new(cell, piece.field_elem()));
// Assign cumulative sum such that for 0 <= i < n,
// z_i = 2^K * z_{i + 1} + m_{i + 1}
// => z_{i + 1} = (z_i - m_{i + 1}) / 2^K
//
// For a message piece m = m_1 + 2^K m_2 + ... + 2^{K(n-1)} m_n}, initialize z_0 = m.
// We end up with z_n = 0. (z_n is not directly encoded as a cell value;
// it is implicitly taken as 0 by adjusting the definition of m_{i+1}.)
let mut z = piece.field_elem();
let inv_2_k = pallas::Base::from_bytes(&INV_TWO_POW_K).unwrap();
// We do not assign the final z_n as it is constrained to be zero.
for (idx, word) in words[0..(words.len() - 1)].iter().enumerate() {
// z_{i + 1} = (z_i - m_{i + 1}) / 2^K
z = z
.zip(*word)
.map(|(z, word)| (z - pallas::Base::from_u64(word as u64)) * inv_2_k);
let cell = region.assign_advice(
|| format!("z_{:?}", idx + 1),
config.bits,
offset + idx + 1,
|| z.ok_or(Error::SynthesisError),
)?;
zs.push(CellValue::new(cell, z))
}
zs
};
// The accumulator x-coordinate provided by the caller MUST have been assigned
// within this region.
let generators: Vec<Option<(pallas::Base, pallas::Base)>> =
if let Some(generators) = generators {
generators.into_iter().map(Some).collect()
} else {
vec![None; piece.num_words()]
};
for (row, gen) in generators.iter().enumerate() {
let x_p = gen.map(|gen| gen.0);
let y_p = gen.map(|gen| gen.1);
// Assign `x_p`
region.assign_advice(
|| "x_p",
config.x_p,
offset + row,
|| x_p.ok_or(Error::SynthesisError),
)?;
// Compute and assign `lambda_1`
let lambda_1 = {
let lambda_1 = x_a
.value()
.zip(y_a.0)
.zip(x_p)
.zip(y_p)
.map(|(((x_a, y_a), x_p), y_p)| (y_a - y_p) * (x_a - x_p).invert().unwrap());
// Assign lambda_1
region.assign_advice(
|| "lambda_1",
config.lambda_1,
offset + row,
|| lambda_1.ok_or(Error::SynthesisError),
)?;
lambda_1
};
// Compute `x_r`
let x_r = lambda_1
.zip(x_a.value())
.zip(x_p)
.map(|((lambda_1, x_a), x_p)| lambda_1.square() - x_a - x_p);
// Compute and assign `lambda_2`
let lambda_2 = {
let lambda_2 = x_a.value().zip(y_a.0).zip(x_r).zip(lambda_1).map(
|(((x_a, y_a), x_r), lambda_1)| {
pallas::Base::from_u64(2) * y_a * (x_a - x_r).invert().unwrap() - lambda_1
},
);
region.assign_advice(
|| "lambda_2",
config.lambda_2,
offset + row,
|| lambda_2.ok_or(Error::SynthesisError),
)?;
lambda_2
};
// Compute and assign `x_a` for the next row.
let x_a_new: X<pallas::Base> = {
let x_a_new = lambda_2
.zip(x_a.value())
.zip(x_r)
.map(|((lambda_2, x_a), x_r)| lambda_2.square() - x_a - x_r);
let x_a_cell = region.assign_advice(
|| "x_a",
config.x_a,
offset + row + 1,
|| x_a_new.ok_or(Error::SynthesisError),
)?;
CellValue::new(x_a_cell, x_a_new).into()
};
// Compute y_a for the next row.
let y_a_new: Y<pallas::Base> = lambda_2
.zip(x_a.value())
.zip(x_a_new.value())
.zip(y_a.0)
.map(|(((lambda_2, x_a), x_a_new), y_a)| lambda_2 * (x_a - x_a_new) - y_a)
.into();
// Update the mutable `x_a`, `y_a` variables.
x_a = x_a_new;
y_a = y_a_new;
}
Ok((x_a, y_a, zs))
}
}
/// The x-coordinate of the accumulator in a Sinsemilla hash instance.
struct X<F: FieldExt>(CellValue<F>);
impl<F: FieldExt> From<CellValue<F>> for X<F> {
fn from(cell_value: CellValue<F>) -> Self {
X(cell_value)
}
}
impl<F: FieldExt> Deref for X<F> {
type Target = CellValue<F>;
fn deref(&self) -> &CellValue<F> {
&self.0
}
}
/// The y-coordinate of the accumulator in a Sinsemilla hash instance.
///
/// This is never actually witnessed until the last round, since it
/// can be derived from other variables. Thus it only exists as a field
/// element, not a `CellValue`.
struct Y<F: FieldExt>(Option<F>);
impl<F: FieldExt> From<Option<F>> for Y<F> {
fn from(value: Option<F>) -> Self {
Y(value)
}
}
impl<F: FieldExt> Deref for Y<F> {
type Target = Option<F>;
fn deref(&self) -> &Option<F> {
&self.0
}
}

View File

@ -0,0 +1,71 @@
//! Gadget and chips for the Sinsemilla hash function.
use crate::circuit::gadget::utilities::{CellValue, Var};
use ff::PrimeFieldBits;
use halo2::{arithmetic::FieldExt, circuit::Cell};
use std::fmt::Debug;
/// A [`Message`] composed of several [`MessagePiece`]s.
#[derive(Clone, Debug)]
pub struct Message<F: FieldExt, const K: usize, const MAX_WORDS: usize>(Vec<MessagePiece<F, K>>);
impl<F: FieldExt + PrimeFieldBits, const K: usize, const MAX_WORDS: usize>
From<Vec<MessagePiece<F, K>>> for Message<F, K, MAX_WORDS>
{
fn from(pieces: Vec<MessagePiece<F, K>>) -> Self {
// A message cannot contain more than `MAX_WORDS` words.
assert!(pieces.iter().map(|piece| piece.num_words()).sum::<usize>() < MAX_WORDS);
Message(pieces)
}
}
impl<F: FieldExt + PrimeFieldBits, const K: usize, const MAX_WORDS: usize> std::ops::Deref
for Message<F, K, MAX_WORDS>
{
type Target = [MessagePiece<F, K>];
fn deref(&self) -> &[MessagePiece<F, K>] {
&self.0
}
}
/// A [`MessagePiece`] of some bitlength.
///
/// The piece must fit within a base field element, which means its length
/// cannot exceed the base field's `NUM_BITS`.
#[derive(Copy, Clone, Debug)]
pub struct MessagePiece<F: FieldExt, const K: usize> {
cell: Cell,
field_elem: Option<F>,
/// The number of K-bit words in this message piece.
num_words: usize,
}
#[allow(clippy::from_over_into)]
impl<F: FieldExt + PrimeFieldBits, const K: usize> Into<CellValue<F>> for MessagePiece<F, K> {
fn into(self) -> CellValue<F> {
CellValue::new(self.cell(), self.field_elem())
}
}
impl<F: FieldExt + PrimeFieldBits, const K: usize> MessagePiece<F, K> {
pub fn new(cell: Cell, field_elem: Option<F>, num_words: usize) -> Self {
assert!(num_words * K < F::NUM_BITS as usize);
Self {
cell,
field_elem,
num_words,
}
}
pub fn num_words(&self) -> usize {
self.num_words
}
pub fn cell(&self) -> Cell {
self.cell
}
pub fn field_elem(&self) -> Option<F> {
self.field_elem
}
}

View File

@ -14,7 +14,7 @@ mod constants;
mod sinsemilla_s;
pub use constants::*;
fn lebs2ip_k(bits: &[bool]) -> u32 {
pub(crate) fn lebs2ip_k(bits: &[bool]) -> u32 {
assert!(bits.len() == K);
bits.iter()
.enumerate()

View File

@ -1,6 +1,7 @@
//! Sinsemilla generators
use group::Curve;
use halo2::arithmetic::{CurveAffine, CurveExt};
use pasta_curves::pallas;
/// Number of bits of each message piece in $\mathsf{SinsemillaHashToPoint}$
pub const K: usize = 10;
@ -62,8 +63,8 @@ pub const Q_MERKLE_CRH: ([u8; 32], [u8; 32]) = (
pub const S_PERSONALIZATION: &str = "z.cash:SinsemillaS";
/// Creates the Sinsemilla S generators used in each round of the Sinsemilla hash
pub fn sinsemilla_s_generators<C: CurveAffine>() -> impl Iterator<Item = (C::Base, C::Base)> {
let hasher = C::CurveExt::hash_to_curve(S_PERSONALIZATION);
pub fn sinsemilla_s_generators() -> impl Iterator<Item = (pallas::Base, pallas::Base)> {
let hasher = pallas::Point::hash_to_curve(S_PERSONALIZATION);
(0..(1u32 << K)).map(move |j| {
let point = hasher(&j.to_le_bytes()).to_affine().coordinates().unwrap();
(*point.x(), *point.y())
@ -84,7 +85,7 @@ mod tests {
#[test]
fn sinsemilla_s() {
use super::super::sinsemilla_s::SINSEMILLA_S;
let sinsemilla_s: Vec<_> = sinsemilla_s_generators::<pallas::Affine>().collect();
let sinsemilla_s: Vec<_> = sinsemilla_s_generators().collect();
assert_eq!(sinsemilla_s.len(), SINSEMILLA_S.len());
for (expected, actual) in sinsemilla_s.iter().zip(&SINSEMILLA_S[..]) {
assert_eq!(expected, actual);