diff --git a/Cargo.lock b/Cargo.lock index df18fe8f75..e047e18189 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -852,6 +852,7 @@ dependencies = [ "criterion", "ff", "group", + "halo2_poseidon", "halo2_proofs", "inferno", "lazy_static", @@ -874,6 +875,12 @@ checksum = "47716fe1ae67969c5e0b2ef826f32db8c3be72be325e1aa3c1951d06b5575ec5" [[package]] name = "halo2_poseidon" version = "0.0.0" +dependencies = [ + "bitvec", + "ff", + "group", + "pasta_curves", +] [[package]] name = "halo2_proofs" diff --git a/halo2_gadgets/Cargo.toml b/halo2_gadgets/Cargo.toml index b22e3e693e..3367b53bc4 100644 --- a/halo2_gadgets/Cargo.toml +++ b/halo2_gadgets/Cargo.toml @@ -26,6 +26,7 @@ arrayvec = "0.7.0" bitvec = "1" ff = "0.13" group = "0.13" +halo2_poseidon = { version = "0.0", path = "../halo2_poseidon", default-features = false } halo2_proofs = { version = "0.3", path = "../halo2_proofs", default-features = false } lazy_static = "1" pasta_curves = "0.5" @@ -40,6 +41,7 @@ plotters = { version = "0.3.0", default-features = false, optional = true } [dev-dependencies] criterion = "0.3" +halo2_poseidon = { version = "0.0", path = "../halo2_poseidon", default-features = false, features = ["test-dependencies"] } proptest = "1.0.0" sinsemilla = { version = "0.1", features = ["test-dependencies"] } diff --git a/halo2_gadgets/src/poseidon.rs b/halo2_gadgets/src/poseidon.rs index 77736c8677..34c1daa9e1 100644 --- a/halo2_gadgets/src/poseidon.rs +++ b/halo2_gadgets/src/poseidon.rs @@ -1,6 +1,5 @@ //! The Poseidon algebraic hash function. -use std::convert::TryInto; use std::fmt; use std::marker::PhantomData; @@ -13,7 +12,7 @@ use halo2_proofs::{ mod pow5; pub use pow5::{Pow5Chip, Pow5Config, StateWord}; -pub mod primitives; +pub use ::halo2_poseidon as primitives; use primitives::{Absorbing, ConstantLength, Domain, Spec, SpongeMode, Squeezing, State}; /// A word from the padded input to a Poseidon sponge. @@ -148,13 +147,7 @@ impl< pub fn new(chip: PoseidonChip, mut layouter: impl Layouter) -> Result { chip.initial_state(&mut layouter).map(|state| Sponge { chip, - mode: Absorbing( - (0..RATE) - .map(|_| None) - .collect::>() - .try_into() - .unwrap(), - ), + mode: Absorbing::init_empty(), state, _marker: PhantomData::default(), }) @@ -166,12 +159,10 @@ impl< mut layouter: impl Layouter, value: PaddedWord, ) -> Result<(), Error> { - for entry in self.mode.0.iter_mut() { - if entry.is_none() { - *entry = Some(value); - return Ok(()); - } - } + let value = match self.mode.absorb(value) { + Ok(()) => return Ok(()), + Err(value) => value, + }; // We've already absorbed as many elements as we can let _ = poseidon_sponge( @@ -180,7 +171,8 @@ impl< &mut self.state, Some(&self.mode), )?; - self.mode = Absorbing::init_with(value); + self.mode = Absorbing::init_empty(); + self.mode.absorb(value).expect("state is not full"); Ok(()) } @@ -220,10 +212,8 @@ impl< /// Squeezes an element from the sponge. pub fn squeeze(&mut self, mut layouter: impl Layouter) -> Result, Error> { loop { - for entry in self.mode.0.iter_mut() { - if let Some(inner) = entry.take() { - return Ok(inner.into()); - } + if let Some(value) = self.mode.squeeze() { + return Ok(value.into()); } // We've already squeezed out all available elements diff --git a/halo2_gadgets/src/poseidon/pow5.rs b/halo2_gadgets/src/poseidon/pow5.rs index 704bda1daa..5c68a2c9ce 100644 --- a/halo2_gadgets/src/poseidon/pow5.rs +++ b/halo2_gadgets/src/poseidon/pow5.rs @@ -340,19 +340,20 @@ impl< let initial_state = initial_state?; // Load the input into this region. - let load_input_word = |i: usize| { - let (cell, value) = match input.0[i].clone() { + let load_input_word = |(i, input_word): (usize, &Option>)| { + let (cell, value) = match input_word { Some(PaddedWord::Message(word)) => (word.cell(), word.value().copied()), Some(PaddedWord::Padding(padding_value)) => { + let value = Value::known(*padding_value); let cell = region .assign_fixed( || format!("load pad_{}", i), config.rc_b[i], 1, - || Value::known(padding_value), + || value, )? .cell(); - (cell, Value::known(padding_value)) + (cell, value) } _ => panic!("Input is not padded"), }; @@ -366,7 +367,12 @@ impl< Ok(StateWord(var)) }; - let input: Result, Error> = (0..RATE).map(load_input_word).collect(); + let input: Result, Error> = input + .expose_inner() + .iter() + .enumerate() + .map(load_input_word) + .collect(); let input = input?; // Constrain the output. @@ -394,14 +400,8 @@ impl< } fn get_output(state: &State) -> Squeezing { - Squeezing( - state[..RATE] - .iter() - .map(|word| Some(word.clone())) - .collect::>() - .try_into() - .unwrap(), - ) + let vals = state[..RATE].to_vec(); + Squeezing::init_full(vals.try_into().expect("correct length")) } } @@ -687,7 +687,7 @@ mod tests { .try_into() .unwrap(); let (round_constants, mds, _) = S::constants(); - poseidon::permute::<_, S, WIDTH, RATE>( + poseidon::test_only_permute::<_, S, WIDTH, RATE>( &mut expected_final_state, &mds, &round_constants, diff --git a/halo2_gadgets/src/poseidon/primitives.rs b/halo2_gadgets/src/poseidon/primitives.rs deleted file mode 100644 index 6c8d133dc2..0000000000 --- a/halo2_gadgets/src/poseidon/primitives.rs +++ /dev/null @@ -1,405 +0,0 @@ -//! The Poseidon algebraic hash function. - -use std::convert::TryInto; -use std::fmt; -use std::iter; -use std::marker::PhantomData; - -use group::ff::{Field, FromUniformBytes, PrimeField}; - -pub(crate) mod fp; -pub(crate) mod fq; -pub(crate) mod grain; -pub(crate) mod mds; - -#[cfg(test)] -pub(crate) mod test_vectors; - -mod p128pow5t3; -pub use p128pow5t3::P128Pow5T3; - -use grain::SboxType; - -/// The type used to hold permutation state. -pub(crate) type State = [F; T]; - -/// The type used to hold sponge rate. -pub(crate) type SpongeRate = [Option; RATE]; - -/// The type used to hold the MDS matrix and its inverse. -pub type Mds = [[F; T]; T]; - -/// A specification for a Poseidon permutation. -pub trait Spec: fmt::Debug { - /// The number of full rounds for this specification. - /// - /// This must be an even number. - fn full_rounds() -> usize; - - /// The number of partial rounds for this specification. - fn partial_rounds() -> usize; - - /// The S-box for this specification. - fn sbox(val: F) -> F; - - /// Side-loaded index of the first correct and secure MDS that will be generated by - /// the reference implementation. - /// - /// This is used by the default implementation of [`Spec::constants`]. If you are - /// hard-coding the constants, you may leave this unimplemented. - fn secure_mds() -> usize; - - /// Generates `(round_constants, mds, mds^-1)` corresponding to this specification. - fn constants() -> (Vec<[F; T]>, Mds, Mds); -} - -/// Generates `(round_constants, mds, mds^-1)` corresponding to this specification. -pub fn generate_constants< - F: FromUniformBytes<64> + Ord, - S: Spec, - const T: usize, - const RATE: usize, ->() -> (Vec<[F; T]>, Mds, Mds) { - let r_f = S::full_rounds(); - let r_p = S::partial_rounds(); - - let mut grain = grain::Grain::new(SboxType::Pow, T as u16, r_f as u16, r_p as u16); - - let round_constants = (0..(r_f + r_p)) - .map(|_| { - let mut rc_row = [F::ZERO; T]; - for (rc, value) in rc_row - .iter_mut() - .zip((0..T).map(|_| grain.next_field_element())) - { - *rc = value; - } - rc_row - }) - .collect(); - - let (mds, mds_inv) = mds::generate_mds::(&mut grain, S::secure_mds()); - - (round_constants, mds, mds_inv) -} - -/// Runs the Poseidon permutation on the given state. -pub(crate) fn permute, const T: usize, const RATE: usize>( - state: &mut State, - mds: &Mds, - round_constants: &[[F; T]], -) { - let r_f = S::full_rounds() / 2; - let r_p = S::partial_rounds(); - - let apply_mds = |state: &mut State| { - let mut new_state = [F::ZERO; T]; - // Matrix multiplication - #[allow(clippy::needless_range_loop)] - for i in 0..T { - for j in 0..T { - new_state[i] += mds[i][j] * state[j]; - } - } - *state = new_state; - }; - - let full_round = |state: &mut State, rcs: &[F; T]| { - for (word, rc) in state.iter_mut().zip(rcs.iter()) { - *word = S::sbox(*word + rc); - } - apply_mds(state); - }; - - let part_round = |state: &mut State, rcs: &[F; T]| { - for (word, rc) in state.iter_mut().zip(rcs.iter()) { - *word += rc; - } - // In a partial round, the S-box is only applied to the first state word. - state[0] = S::sbox(state[0]); - apply_mds(state); - }; - - iter::empty() - .chain(iter::repeat(&full_round as &dyn Fn(&mut State, &[F; T])).take(r_f)) - .chain(iter::repeat(&part_round as &dyn Fn(&mut State, &[F; T])).take(r_p)) - .chain(iter::repeat(&full_round as &dyn Fn(&mut State, &[F; T])).take(r_f)) - .zip(round_constants.iter()) - .fold(state, |state, (round, rcs)| { - round(state, rcs); - state - }); -} - -fn poseidon_sponge, const T: usize, const RATE: usize>( - state: &mut State, - input: Option<&Absorbing>, - mds_matrix: &Mds, - round_constants: &[[F; T]], -) -> Squeezing { - if let Some(Absorbing(input)) = input { - // `Iterator::zip` short-circuits when one iterator completes, so this will only - // mutate the rate portion of the state. - for (word, value) in state.iter_mut().zip(input.iter()) { - *word += value.expect("poseidon_sponge is called with a padded input"); - } - } - - permute::(state, mds_matrix, round_constants); - - let mut output = [None; RATE]; - for (word, value) in output.iter_mut().zip(state.iter()) { - *word = Some(*value); - } - Squeezing(output) -} - -mod private { - pub trait SealedSpongeMode {} - impl SealedSpongeMode for super::Absorbing {} - impl SealedSpongeMode for super::Squeezing {} -} - -/// The state of the `Sponge`. -pub trait SpongeMode: private::SealedSpongeMode {} - -/// The absorbing state of the `Sponge`. -#[derive(Debug)] -pub struct Absorbing(pub(crate) SpongeRate); - -/// The squeezing state of the `Sponge`. -#[derive(Debug)] -pub struct Squeezing(pub(crate) SpongeRate); - -impl SpongeMode for Absorbing {} -impl SpongeMode for Squeezing {} - -impl Absorbing { - pub(crate) fn init_with(val: F) -> Self { - Self( - iter::once(Some(val)) - .chain((1..RATE).map(|_| None)) - .collect::>() - .try_into() - .unwrap(), - ) - } -} - -/// A Poseidon sponge. -pub(crate) struct Sponge< - F: Field, - S: Spec, - M: SpongeMode, - const T: usize, - const RATE: usize, -> { - mode: M, - state: State, - mds_matrix: Mds, - round_constants: Vec<[F; T]>, - _marker: PhantomData, -} - -impl, const T: usize, const RATE: usize> - Sponge, T, RATE> -{ - /// Constructs a new sponge for the given Poseidon specification. - pub(crate) fn new(initial_capacity_element: F) -> Self { - let (round_constants, mds_matrix, _) = S::constants(); - - let mode = Absorbing([None; RATE]); - let mut state = [F::ZERO; T]; - state[RATE] = initial_capacity_element; - - Sponge { - mode, - state, - mds_matrix, - round_constants, - _marker: PhantomData::default(), - } - } - - /// Absorbs an element into the sponge. - pub(crate) fn absorb(&mut self, value: F) { - for entry in self.mode.0.iter_mut() { - if entry.is_none() { - *entry = Some(value); - return; - } - } - - // We've already absorbed as many elements as we can - let _ = poseidon_sponge::( - &mut self.state, - Some(&self.mode), - &self.mds_matrix, - &self.round_constants, - ); - self.mode = Absorbing::init_with(value); - } - - /// Transitions the sponge into its squeezing state. - pub(crate) fn finish_absorbing(mut self) -> Sponge, T, RATE> { - let mode = poseidon_sponge::( - &mut self.state, - Some(&self.mode), - &self.mds_matrix, - &self.round_constants, - ); - - Sponge { - mode, - state: self.state, - mds_matrix: self.mds_matrix, - round_constants: self.round_constants, - _marker: PhantomData::default(), - } - } -} - -impl, const T: usize, const RATE: usize> - Sponge, T, RATE> -{ - /// Squeezes an element from the sponge. - pub(crate) fn squeeze(&mut self) -> F { - loop { - for entry in self.mode.0.iter_mut() { - if let Some(e) = entry.take() { - return e; - } - } - - // We've already squeezed out all available elements - self.mode = poseidon_sponge::( - &mut self.state, - None, - &self.mds_matrix, - &self.round_constants, - ); - } - } -} - -/// A domain in which a Poseidon hash function is being used. -pub trait Domain { - /// Iterator that outputs padding field elements. - type Padding: IntoIterator; - - /// The name of this domain, for debug formatting purposes. - fn name() -> String; - - /// The initial capacity element, encoding this domain. - fn initial_capacity_element() -> F; - - /// Returns the padding to be appended to the input. - fn padding(input_len: usize) -> Self::Padding; -} - -/// A Poseidon hash function used with constant input length. -/// -/// Domain specified in [ePrint 2019/458 section 4.2](https://eprint.iacr.org/2019/458.pdf). -#[derive(Clone, Copy, Debug)] -pub struct ConstantLength; - -impl Domain for ConstantLength { - type Padding = iter::Take>; - - fn name() -> String { - format!("ConstantLength<{}>", L) - } - - fn initial_capacity_element() -> F { - // Capacity value is $length \cdot 2^64 + (o-1)$ where o is the output length. - // We hard-code an output length of 1. - F::from_u128((L as u128) << 64) - } - - fn padding(input_len: usize) -> Self::Padding { - assert_eq!(input_len, L); - // For constant-input-length hashing, we pad the input with zeroes to a multiple - // of RATE. On its own this would not be sponge-compliant padding, but the - // Poseidon authors encode the constant length into the capacity element, ensuring - // that inputs of different lengths do not share the same permutation. - let k = (L + RATE - 1) / RATE; - iter::repeat(F::ZERO).take(k * RATE - L) - } -} - -/// A Poseidon hash function, built around a sponge. -pub struct Hash< - F: Field, - S: Spec, - D: Domain, - const T: usize, - const RATE: usize, -> { - sponge: Sponge, T, RATE>, - _domain: PhantomData, -} - -impl, D: Domain, const T: usize, const RATE: usize> - fmt::Debug for Hash -{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Hash") - .field("width", &T) - .field("rate", &RATE) - .field("R_F", &S::full_rounds()) - .field("R_P", &S::partial_rounds()) - .field("domain", &D::name()) - .finish() - } -} - -impl, D: Domain, const T: usize, const RATE: usize> - Hash -{ - /// Initializes a new hasher. - pub fn init() -> Self { - Hash { - sponge: Sponge::new(D::initial_capacity_element()), - _domain: PhantomData::default(), - } - } -} - -impl, const T: usize, const RATE: usize, const L: usize> - Hash, T, RATE> -{ - /// Hashes the given input. - pub fn hash(mut self, message: [F; L]) -> F { - for value in message - .into_iter() - .chain( as Domain>::padding(L)) - { - self.sponge.absorb(value); - } - self.sponge.finish_absorbing().squeeze() - } -} - -#[cfg(test)] -mod tests { - use group::ff::PrimeField; - use pasta_curves::pallas; - - use super::{permute, ConstantLength, Hash, P128Pow5T3 as OrchardNullifier, Spec}; - - #[test] - fn orchard_spec_equivalence() { - let message = [pallas::Base::from(6), pallas::Base::from(42)]; - - let (round_constants, mds, _) = OrchardNullifier::constants(); - - let hasher = Hash::<_, OrchardNullifier, ConstantLength<2>, 3, 2>::init(); - let result = hasher.hash(message); - - // The result should be equivalent to just directly applying the permutation and - // taking the first state element as the output. - let mut state = [message[0], message[1], pallas::Base::from_u128(2 << 64)]; - permute::<_, OrchardNullifier, 3, 2>(&mut state, &mds, &round_constants); - assert_eq!(state[0], result); - } -} diff --git a/halo2_poseidon/Cargo.toml b/halo2_poseidon/Cargo.toml index c975ae4839..2831342e1b 100644 --- a/halo2_poseidon/Cargo.toml +++ b/halo2_poseidon/Cargo.toml @@ -15,3 +15,10 @@ readme = "README.md" categories = ["cryptography"] [dependencies] +bitvec = "1" +ff = "0.13" +group = "0.13" +pasta_curves = "0.5" + +[features] +test-dependencies = [] diff --git a/halo2_gadgets/src/poseidon/primitives/fp.rs b/halo2_poseidon/src/fp.rs similarity index 100% rename from halo2_gadgets/src/poseidon/primitives/fp.rs rename to halo2_poseidon/src/fp.rs diff --git a/halo2_gadgets/src/poseidon/primitives/fq.rs b/halo2_poseidon/src/fq.rs similarity index 100% rename from halo2_gadgets/src/poseidon/primitives/fq.rs rename to halo2_poseidon/src/fq.rs diff --git a/halo2_gadgets/src/poseidon/primitives/grain.rs b/halo2_poseidon/src/grain.rs similarity index 100% rename from halo2_gadgets/src/poseidon/primitives/grain.rs rename to halo2_poseidon/src/grain.rs diff --git a/halo2_poseidon/src/lib.rs b/halo2_poseidon/src/lib.rs index e69de29bb2..7745db9f3d 100644 --- a/halo2_poseidon/src/lib.rs +++ b/halo2_poseidon/src/lib.rs @@ -0,0 +1,483 @@ +//! The Poseidon algebraic hash function. + +use std::convert::TryInto; +use std::fmt; +use std::iter; +use std::marker::PhantomData; + +use group::ff::{Field, FromUniformBytes, PrimeField}; + +pub(crate) mod fp; +pub(crate) mod fq; +pub(crate) mod grain; +pub(crate) mod mds; + +#[cfg(any(test, feature = "test-dependencies"))] +pub mod test_vectors; + +mod p128pow5t3; +pub use p128pow5t3::P128Pow5T3; + +use grain::SboxType; + +/// The type used to hold permutation state. +pub type State = [F; T]; + +/// The type used to hold sponge rate. +pub(crate) type SpongeRate = [Option; RATE]; + +/// The type used to hold the MDS matrix and its inverse. +pub type Mds = [[F; T]; T]; + +/// A specification for a Poseidon permutation. +pub trait Spec: fmt::Debug { + /// The number of full rounds for this specification. + /// + /// This must be an even number. + fn full_rounds() -> usize; + + /// The number of partial rounds for this specification. + fn partial_rounds() -> usize; + + /// The S-box for this specification. + fn sbox(val: F) -> F; + + /// Side-loaded index of the first correct and secure MDS that will be generated by + /// the reference implementation. + /// + /// This is used by the default implementation of [`Spec::constants`]. If you are + /// hard-coding the constants, you may leave this unimplemented. + fn secure_mds() -> usize; + + /// Generates `(round_constants, mds, mds^-1)` corresponding to this specification. + fn constants() -> (Vec<[F; T]>, Mds, Mds); +} + +/// Generates `(round_constants, mds, mds^-1)` corresponding to this specification. +pub fn generate_constants< + F: FromUniformBytes<64> + Ord, + S: Spec, + const T: usize, + const RATE: usize, +>() -> (Vec<[F; T]>, Mds, Mds) { + let r_f = S::full_rounds(); + let r_p = S::partial_rounds(); + + let mut grain = grain::Grain::new(SboxType::Pow, T as u16, r_f as u16, r_p as u16); + + let round_constants = (0..(r_f + r_p)) + .map(|_| { + let mut rc_row = [F::ZERO; T]; + for (rc, value) in rc_row + .iter_mut() + .zip((0..T).map(|_| grain.next_field_element())) + { + *rc = value; + } + rc_row + }) + .collect(); + + let (mds, mds_inv) = mds::generate_mds::(&mut grain, S::secure_mds()); + + (round_constants, mds, mds_inv) +} + +/// Runs the Poseidon permutation on the given state. +/// +/// Exposed for testing purposes only. +#[cfg(feature = "test-dependencies")] +pub fn test_only_permute, const T: usize, const RATE: usize>( + state: &mut State, + mds: &Mds, + round_constants: &[[F; T]], +) { + permute::(state, mds, round_constants); +} + +/// Runs the Poseidon permutation on the given state. +pub(crate) fn permute, const T: usize, const RATE: usize>( + state: &mut State, + mds: &Mds, + round_constants: &[[F; T]], +) { + let r_f = S::full_rounds() / 2; + let r_p = S::partial_rounds(); + + let apply_mds = |state: &mut State| { + let mut new_state = [F::ZERO; T]; + // Matrix multiplication + #[allow(clippy::needless_range_loop)] + for i in 0..T { + for j in 0..T { + new_state[i] += mds[i][j] * state[j]; + } + } + *state = new_state; + }; + + let full_round = |state: &mut State, rcs: &[F; T]| { + for (word, rc) in state.iter_mut().zip(rcs.iter()) { + *word = S::sbox(*word + rc); + } + apply_mds(state); + }; + + let part_round = |state: &mut State, rcs: &[F; T]| { + for (word, rc) in state.iter_mut().zip(rcs.iter()) { + *word += rc; + } + // In a partial round, the S-box is only applied to the first state word. + state[0] = S::sbox(state[0]); + apply_mds(state); + }; + + iter::empty() + .chain(iter::repeat(&full_round as &dyn Fn(&mut State, &[F; T])).take(r_f)) + .chain(iter::repeat(&part_round as &dyn Fn(&mut State, &[F; T])).take(r_p)) + .chain(iter::repeat(&full_round as &dyn Fn(&mut State, &[F; T])).take(r_f)) + .zip(round_constants.iter()) + .fold(state, |state, (round, rcs)| { + round(state, rcs); + state + }); +} + +fn poseidon_sponge, const T: usize, const RATE: usize>( + state: &mut State, + input: Option<&Absorbing>, + mds_matrix: &Mds, + round_constants: &[[F; T]], +) -> Squeezing { + if let Some(Absorbing(input)) = input { + // `Iterator::zip` short-circuits when one iterator completes, so this will only + // mutate the rate portion of the state. + for (word, value) in state.iter_mut().zip(input.iter()) { + *word += value.expect("poseidon_sponge is called with a padded input"); + } + } + + permute::(state, mds_matrix, round_constants); + + let mut output = [None; RATE]; + for (word, value) in output.iter_mut().zip(state.iter()) { + *word = Some(*value); + } + Squeezing(output) +} + +mod private { + pub trait SealedSpongeMode {} + impl SealedSpongeMode for super::Absorbing {} + impl SealedSpongeMode for super::Squeezing {} +} + +/// The state of the `Sponge`. +pub trait SpongeMode: private::SealedSpongeMode {} + +/// The absorbing state of the `Sponge`. +#[derive(Debug)] +pub struct Absorbing(pub(crate) SpongeRate); + +/// The squeezing state of the `Sponge`. +#[derive(Debug)] +pub struct Squeezing(pub(crate) SpongeRate); + +impl SpongeMode for Absorbing {} +impl SpongeMode for Squeezing {} + +impl Absorbing { + pub(crate) fn init_with(val: F) -> Self { + let mut state = Self::init_empty(); + state.absorb(val).expect("state is not full"); + state + } + + /// Initializes an empty sponge in the absorbing state. + pub fn init_empty() -> Self { + Self( + (0..RATE) + .map(|_| None) + .collect::>() + .try_into() + .unwrap(), + ) + } +} + +impl Absorbing { + /// Attempts to absorb a value into the sponge state. + /// + /// Returns the value if it was not absorbed because the sponge is full. + pub fn absorb(&mut self, value: F) -> Result<(), F> { + for entry in self.0.iter_mut() { + if entry.is_none() { + *entry = Some(value); + return Ok(()); + } + } + // Sponge is full. + Err(value) + } + + /// Exposes the inner state of the sponge. + /// + /// This is a low-level API, requiring a detailed understanding of this specific + /// Poseidon implementation to use correctly and securely. It is exposed for use by + /// the circuit implementation in `halo2_gadgets`, and may be removed from the public + /// API if refactoring enables the circuit implementation to move into this crate. + pub fn expose_inner(&self) -> &SpongeRate { + &self.0 + } +} + +impl Squeezing { + /// Initializes a full sponge in the squeezing state. + /// + /// This is a low-level API, requiring a detailed understanding of this specific + /// Poseidon implementation to use correctly and securely. It is exposed for use by + /// the circuit implementation in `halo2_gadgets`, and may be removed from the public + /// API if refactoring enables the circuit implementation to move into this crate. + pub fn init_full(vals: [F; RATE]) -> Self { + Self( + vals.into_iter() + .map(Some) + .collect::>() + .try_into() + .unwrap(), + ) + } +} + +impl Squeezing { + /// Attempts to squeeze a value from the sponge state. + /// + /// Returns `None` if the sponge is empty. + pub fn squeeze(&mut self) -> Option { + for entry in self.0.iter_mut() { + if let Some(inner) = entry.take() { + return Some(inner); + } + } + // Sponge is empty. + None + } +} + +/// A Poseidon sponge. +pub(crate) struct Sponge< + F: Field, + S: Spec, + M: SpongeMode, + const T: usize, + const RATE: usize, +> { + mode: M, + state: State, + mds_matrix: Mds, + round_constants: Vec<[F; T]>, + _marker: PhantomData, +} + +impl, const T: usize, const RATE: usize> + Sponge, T, RATE> +{ + /// Constructs a new sponge for the given Poseidon specification. + pub(crate) fn new(initial_capacity_element: F) -> Self { + let (round_constants, mds_matrix, _) = S::constants(); + + let mode = Absorbing([None; RATE]); + let mut state = [F::ZERO; T]; + state[RATE] = initial_capacity_element; + + Sponge { + mode, + state, + mds_matrix, + round_constants, + _marker: PhantomData::default(), + } + } + + /// Absorbs an element into the sponge. + pub(crate) fn absorb(&mut self, value: F) { + for entry in self.mode.0.iter_mut() { + if entry.is_none() { + *entry = Some(value); + return; + } + } + + // We've already absorbed as many elements as we can + let _ = poseidon_sponge::( + &mut self.state, + Some(&self.mode), + &self.mds_matrix, + &self.round_constants, + ); + self.mode = Absorbing::init_with(value); + } + + /// Transitions the sponge into its squeezing state. + pub(crate) fn finish_absorbing(mut self) -> Sponge, T, RATE> { + let mode = poseidon_sponge::( + &mut self.state, + Some(&self.mode), + &self.mds_matrix, + &self.round_constants, + ); + + Sponge { + mode, + state: self.state, + mds_matrix: self.mds_matrix, + round_constants: self.round_constants, + _marker: PhantomData::default(), + } + } +} + +impl, const T: usize, const RATE: usize> + Sponge, T, RATE> +{ + /// Squeezes an element from the sponge. + pub(crate) fn squeeze(&mut self) -> F { + loop { + for entry in self.mode.0.iter_mut() { + if let Some(e) = entry.take() { + return e; + } + } + + // We've already squeezed out all available elements + self.mode = poseidon_sponge::( + &mut self.state, + None, + &self.mds_matrix, + &self.round_constants, + ); + } + } +} + +/// A domain in which a Poseidon hash function is being used. +pub trait Domain { + /// Iterator that outputs padding field elements. + type Padding: IntoIterator; + + /// The name of this domain, for debug formatting purposes. + fn name() -> String; + + /// The initial capacity element, encoding this domain. + fn initial_capacity_element() -> F; + + /// Returns the padding to be appended to the input. + fn padding(input_len: usize) -> Self::Padding; +} + +/// A Poseidon hash function used with constant input length. +/// +/// Domain specified in [ePrint 2019/458 section 4.2](https://eprint.iacr.org/2019/458.pdf). +#[derive(Clone, Copy, Debug)] +pub struct ConstantLength; + +impl Domain for ConstantLength { + type Padding = iter::Take>; + + fn name() -> String { + format!("ConstantLength<{}>", L) + } + + fn initial_capacity_element() -> F { + // Capacity value is $length \cdot 2^64 + (o-1)$ where o is the output length. + // We hard-code an output length of 1. + F::from_u128((L as u128) << 64) + } + + fn padding(input_len: usize) -> Self::Padding { + assert_eq!(input_len, L); + // For constant-input-length hashing, we pad the input with zeroes to a multiple + // of RATE. On its own this would not be sponge-compliant padding, but the + // Poseidon authors encode the constant length into the capacity element, ensuring + // that inputs of different lengths do not share the same permutation. + let k = (L + RATE - 1) / RATE; + iter::repeat(F::ZERO).take(k * RATE - L) + } +} + +/// A Poseidon hash function, built around a sponge. +pub struct Hash< + F: Field, + S: Spec, + D: Domain, + const T: usize, + const RATE: usize, +> { + sponge: Sponge, T, RATE>, + _domain: PhantomData, +} + +impl, D: Domain, const T: usize, const RATE: usize> + fmt::Debug for Hash +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Hash") + .field("width", &T) + .field("rate", &RATE) + .field("R_F", &S::full_rounds()) + .field("R_P", &S::partial_rounds()) + .field("domain", &D::name()) + .finish() + } +} + +impl, D: Domain, const T: usize, const RATE: usize> + Hash +{ + /// Initializes a new hasher. + pub fn init() -> Self { + Hash { + sponge: Sponge::new(D::initial_capacity_element()), + _domain: PhantomData::default(), + } + } +} + +impl, const T: usize, const RATE: usize, const L: usize> + Hash, T, RATE> +{ + /// Hashes the given input. + pub fn hash(mut self, message: [F; L]) -> F { + for value in message + .into_iter() + .chain( as Domain>::padding(L)) + { + self.sponge.absorb(value); + } + self.sponge.finish_absorbing().squeeze() + } +} + +#[cfg(test)] +mod tests { + use group::ff::PrimeField; + use pasta_curves::pallas; + + use super::{permute, ConstantLength, Hash, P128Pow5T3 as OrchardNullifier, Spec}; + + #[test] + fn orchard_spec_equivalence() { + let message = [pallas::Base::from(6), pallas::Base::from(42)]; + + let (round_constants, mds, _) = OrchardNullifier::constants(); + + let hasher = Hash::<_, OrchardNullifier, ConstantLength<2>, 3, 2>::init(); + let result = hasher.hash(message); + + // The result should be equivalent to just directly applying the permutation and + // taking the first state element as the output. + let mut state = [message[0], message[1], pallas::Base::from_u128(2 << 64)]; + permute::<_, OrchardNullifier, 3, 2>(&mut state, &mds, &round_constants); + assert_eq!(state[0], result); + } +} diff --git a/halo2_gadgets/src/poseidon/primitives/mds.rs b/halo2_poseidon/src/mds.rs similarity index 100% rename from halo2_gadgets/src/poseidon/primitives/mds.rs rename to halo2_poseidon/src/mds.rs diff --git a/halo2_gadgets/src/poseidon/primitives/p128pow5t3.rs b/halo2_poseidon/src/p128pow5t3.rs similarity index 95% rename from halo2_gadgets/src/poseidon/primitives/p128pow5t3.rs rename to halo2_poseidon/src/p128pow5t3.rs index ff5317406f..c7640cd7a0 100644 --- a/halo2_gadgets/src/poseidon/primitives/p128pow5t3.rs +++ b/halo2_poseidon/src/p128pow5t3.rs @@ -1,4 +1,4 @@ -use halo2_proofs::arithmetic::Field; +use ff::Field; use pasta_curves::{pallas::Base as Fp, vesta::Base as Fq}; use super::{Mds, Spec}; @@ -73,9 +73,7 @@ mod tests { super::{fp, fq}, Fp, Fq, }; - use crate::poseidon::primitives::{ - generate_constants, permute, ConstantLength, Hash, Mds, Spec, - }; + use crate::{generate_constants, permute, ConstantLength, Hash, Mds, Spec}; /// The same Poseidon specification as poseidon::P128Pow5T3, but constructed /// such that its constants will be generated at runtime. @@ -257,7 +255,7 @@ mod tests { { let (round_constants, mds, _) = super::P128Pow5T3::constants(); - for tv in crate::poseidon::primitives::test_vectors::fp::permute() { + for tv in crate::test_vectors::fp::permute() { let mut state = [ Fp::from_repr(tv.initial_state[0]).unwrap(), Fp::from_repr(tv.initial_state[1]).unwrap(), @@ -275,7 +273,7 @@ mod tests { { let (round_constants, mds, _) = super::P128Pow5T3::constants(); - for tv in crate::poseidon::primitives::test_vectors::fq::permute() { + for tv in crate::test_vectors::fq::permute() { let mut state = [ Fq::from_repr(tv.initial_state[0]).unwrap(), Fq::from_repr(tv.initial_state[1]).unwrap(), @@ -293,7 +291,7 @@ mod tests { #[test] fn hash_test_vectors() { - for tv in crate::poseidon::primitives::test_vectors::fp::hash() { + for tv in crate::test_vectors::fp::hash() { let message = [ Fp::from_repr(tv.input[0]).unwrap(), Fp::from_repr(tv.input[1]).unwrap(), @@ -305,7 +303,7 @@ mod tests { assert_eq!(result.to_repr(), tv.output); } - for tv in crate::poseidon::primitives::test_vectors::fq::hash() { + for tv in crate::test_vectors::fq::hash() { let message = [ Fq::from_repr(tv.input[0]).unwrap(), Fq::from_repr(tv.input[1]).unwrap(), diff --git a/halo2_gadgets/src/poseidon/primitives/test_vectors.rs b/halo2_poseidon/src/test_vectors.rs similarity index 99% rename from halo2_gadgets/src/poseidon/primitives/test_vectors.rs rename to halo2_poseidon/src/test_vectors.rs index 1c345a4853..d7ec29893f 100644 --- a/halo2_gadgets/src/poseidon/primitives/test_vectors.rs +++ b/halo2_poseidon/src/test_vectors.rs @@ -1,19 +1,19 @@ //! Test vectors for [`OrchardNullifier`]. -pub(crate) struct PermuteTestVector { - pub(crate) initial_state: [[u8; 32]; 3], - pub(crate) final_state: [[u8; 32]; 3], +pub struct PermuteTestVector { + pub initial_state: [[u8; 32]; 3], + pub final_state: [[u8; 32]; 3], } -pub(crate) struct HashTestVector { - pub(crate) input: [[u8; 32]; 2], - pub(crate) output: [u8; 32], +pub struct HashTestVector { + pub input: [[u8; 32]; 2], + pub output: [u8; 32], } -pub(crate) mod fp { +pub mod fp { use super::*; - pub(crate) fn permute() -> Vec { + pub fn permute() -> Vec { use PermuteTestVector as TestVector; // From https://github.com/zcash-hackworks/zcash-test-vectors/blob/master/orchard_poseidon/permute/fp.py @@ -417,7 +417,7 @@ pub(crate) mod fp { ] } - pub(crate) fn hash() -> Vec { + pub fn hash() -> Vec { use HashTestVector as TestVector; // From https://github.com/zcash-hackworks/zcash-test-vectors/blob/master/orchard_poseidon/hash/fp.py @@ -635,10 +635,10 @@ pub(crate) mod fp { } } -pub(crate) mod fq { +pub mod fq { use super::*; - pub(crate) fn permute() -> Vec { + pub fn permute() -> Vec { use PermuteTestVector as TestVector; // From https://github.com/zcash-hackworks/zcash-test-vectors/blob/master/orchard_poseidon/permute/fq.py @@ -1042,7 +1042,7 @@ pub(crate) mod fq { ] } - pub(crate) fn hash() -> Vec { + pub fn hash() -> Vec { use HashTestVector as TestVector; // From https://github.com/zcash-hackworks/zcash-test-vectors/blob/master/orchard_poseidon/hash/fq.py