Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Poseidon hash #114

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft

Poseidon hash #114

wants to merge 4 commits into from

Conversation

GhostOfGauss
Copy link
Collaborator

This PR adds our first hash function--the optimized Poseidon hash, complete with parameter generation written in Rust. Credit goes to @tsunrise and @BoyuanFeng for writing this.  The contents of this PR is adapted from their implementation here.  (See also this spec) The biggest change is the addition of a HashFunction trait and making hashing stateless.

Some work remains, but I'm opening this draft to start collecting feedback.  What remains is documentation and testing.

Feedback is needed on the overall interface with hash functions.  The HashFunction trait is a starting point for discussion. This topic was opened in Issue #109 and discussion should continue there.  

This will close #104

@GhostOfGauss GhostOfGauss added T-feature Type: new features T-gadget Implments a new Plonk gadget labels Feb 4, 2022
@GhostOfGauss GhostOfGauss self-assigned this Feb 4, 2022
@LukePearson1
Copy link
Contributor

Is this PR ready for review @GhostOfGauss? If so, please convert from draft.

@GhostOfGauss
Copy link
Collaborator Author

No, not ready yet

Comment on lines 14 to 15
#[macro_use]
pub extern crate ark_std;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not depend on ark_std and use the core and alloc libraries instead.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, took care of this. There were two spots (here and here ) where Manta's implementation uses ark_ff::vec and I was unsure whether to remove these. It compiles fine without these imports--can you confirm that it's safe to remove them?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ark_ff::vec should be removed in favor of alloc::vec. It's just a re-export.

}
}

trait HashFunction<const WIDTH: usize, COM = ()> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be arity, not width since the notion of "width" is a poseidon-only thing.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, but see comment below regarding const generic expressions

Comment on lines +563 to +565
// The input ought to have size ARITY, but const generic expressions aren't
// allowed yet
fn hash(&self, input: &[Self::Input], compiler: &mut COM) -> Self::Output;
Copy link
Collaborator

@bhgomes bhgomes Feb 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But can we use const-generic expressions? The issue I was referring to is that as you said above, HashFunction trait should have the generic ARITY but within PoseidonSpec we need to arrays whose size is ARITY + 1. That produces "error: generic parameters may not be used in const operations" with the suggestion to "use #![feature(generic_const_exprs)] to allow generic const expressions"

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The notion of width is internal to Poseidon, so the way it's represented in memory is an implementation detail, not something that is exposed to the interface. Using arrays would be nice but since the feature is unstable, the other options are to use a Vec for the state vector (of length ARITY + 1) or to separate the state into two parts like this:

type State = (F, [F; ARITY]);

In either case, the input to the hash function is of the form [F; ARITY], the rest of the state is not part of the interface.

NB: In my implementation in manta-rs I use Vec for simplicity but for the production circuit I'm going to try and use State as above.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a side note, this poseidon implementation use WIDTH (ARITY + 1) instead of ARITY. We can change it to ARITY once generic_const_exprs is stable.
@bhgomes I guess we have to use slice instead of const generics? because

  • as @GhostOfGauss said, cannot evaluate WIDTH - 1 at compile time
  • compiler will probably optimize the overhead out?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tsunrise To make it equivalent to the array case in terms of efficiency, we only need to keep all the data on the stack, so we can use something like (F, [F; ARITY]) as the state vector. This also preserves the input memory format since the state enters the algorithm in these two parts anyway, so copy elision will remove some overhead here that you would have if you had to copy everything into a [F; ARITY + 1] which is more nontrivial to optimize. Maybe the Rust optimizer is very good but I wouldn't rely on optimizations that are not explicitly based on the language semantics like ownership and moving.

type NepArity = generic_array::typenum::U2;

let (nep_consts, ark_consts) =
collect_neptune_constants::<NepArity>(Strength::Standard);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

collect_neptune_constants is missing, as well as neptune dev_dependencies? Nice to have these tests.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is from plonk_prototype repo and we temporarily used neptune library as a reference to check the correctness of our code.

Anyway, we should not even import neptune library here. We need to hardcoded tests.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean hardcoded test vectors. Sounds good.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those hardcoded test vectors are now here: https://github.com/Manta-Network/Plonk-Prototype/blob/d2e25aae7ad4526791f4543cebf1e4df5fbf14ac/src/poseidon/poseidon_ref.rs#L380-L415. Those test vectors come from my modified variants of the original sage script. @GhostOfGauss You can add this test here remove any dependency on Neptune

});
let plonk_hash = poseidon_circuit.output_hash(&mut c);

c.check_circuit_satisfied();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check_circuit_satisfied requires #[cfg(feature = "trace")]

constants: PoseidonConstants<S::ParameterField>,
) -> Self {
let mut elements = S::zeros(c);
elements[0] = S::alloc(c, constants.domain_tag);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This loads constants.domain_tag into a witness variable using c.add_input, but how do we constrain the witness to correspond to the actual constants?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point... a naive way is that for the input phase we can just use an zero_var for element[0] and keep track of domain tag, and then we can handle this in the first round of add_round_constants and s-box.

@LukePearson1 LukePearson1 requested review from lopeetall and removed request for lopeetall February 8, 2022 01:03
Copy link
Collaborator

@tsunrise tsunrise left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks

@@ -0,0 +1,473 @@
//! Library independent specification for field trait.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this file is needed here

Comment on lines +563 to +565
// The input ought to have size ARITY, but const generic expressions aren't
// allowed yet
fn hash(&self, input: &[Self::Input], compiler: &mut COM) -> Self::Output;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a side note, this poseidon implementation use WIDTH (ARITY + 1) instead of ARITY. We can change it to ARITY once generic_const_exprs is stable.
@bhgomes I guess we have to use slice instead of const generics? because

  • as @GhostOfGauss said, cannot evaluate WIDTH - 1 at compile time
  • compiler will probably optimize the overhead out?

constants: PoseidonConstants<S::ParameterField>,
) -> Self {
let mut elements = S::zeros(c);
elements[0] = S::alloc(c, constants.domain_tag);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point... a naive way is that for the input phase we can just use an zero_var for element[0] and keep track of domain tag, and then we can handle this in the first round of add_round_constants and s-box.

)
}

#[test]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we do not need to test on r1cs.

(Indeed, we should remove everything related r1cs here. It's not relevant. )

conversion::cast_field,
neptune_hyper_parameter::collect_neptune_constants,
};
use neptune::{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use some hardcoded test instead of relying on neptune library.

type NepArity = generic_array::typenum::U2;

let (nep_consts, ark_consts) =
collect_neptune_constants::<NepArity>(Strength::Standard);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is from plonk_prototype repo and we temporarily used neptune library as a reference to check the correctness of our code.

Anyway, we should not even import neptune library here. We need to hardcoded tests.

match field {
1 => {
for _ in 0..num_constants {
while {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tsunrise
Copy link
Collaborator

tsunrise commented Feb 12, 2022

@GhostOfGauss The round constant generation code in Manta's plonk prototype repo was incorrect, and I fixed that so it now passes the hardcoded tests. When you have some time can you sync those changes here?

Copy link
Collaborator

@iquerejeta iquerejeta left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comments from a first pass.

impl<F: PrimeField> PoseidonConstants<F> {
/// Generate all constants needed for poseidon hash of specified
/// width. Note that WIDTH = ARITY + 1
pub fn generate<const WIDTH: usize>() -> Self {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to declare WIDTH as a constant to the structure rather than the function? This way we ensure that the poseidon trait and the constants used are initialised for the same width.

if !last_round {
let needed = *const_offset + to_take;
assert!(
needed <= constants.compressed_round_constants.len(),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This overflow could happen in any round, not only the last, right? Might be missing some check performed on the number of constants though. But maybe bounding the constants with the Width (as suggested in a different comment) might help in avoiding these type of overflows.

/// A `SparseMatrix` is specifically one of the form of M''.
/// This means its first row and column are each dense, and the interior matrix
/// (minor to the element in both the row and column) is the identity.
/// We will pluralize this compact structure `sparse_matrixes` to distinguish
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming could be confusing. What about sparse_factorized instead of sparse_matrixes?

//
// // First row is dense.
let tmp = Self::muli(c, &state[0], &matrix.v_rest[j - 1]);
*val = Self::add(c, val, &tmp);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might be missing important points of how constraints are built, but would it be possible to merge both additions into one? First one is after all an addition of zero

*val = Self::add(c, state[j], &tmp);

Copy link
Collaborator

@tsunrise tsunrise Feb 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you are right. I've updated the code in the prototype repo here. I believe those changes will soon be synced to this PR.

/// `w_hat` is the first column of the M'' matrix. It will be directly
/// multiplied (scalar product) with a row of state elements.
pub w_hat: Vec<F>,
/// `v_rest` contains all but the first (already included in `w_hat`).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// `v_rest` contains all but the first (already included in `w_hat`).
/// `v_rest` contains the first row except for the first value (already included in `w_hat`).

pub m_hat: Matrix<F>,
pub m_hat_inv: Matrix<F>,
pub m_prime: Matrix<F>,
pub m_double_prime: Matrix<F>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't this one be defined as sparse?

fn output_hash(
c: &mut COM,
elements: &[Self::Field; WIDTH],
constants: &PoseidonConstants<Self::ParameterField>,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we consider adding an index for the output element here? Or maybe some understanding on why the 2nd element is selected and not the 1st (0-indexed element).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T-feature Type: new features T-gadget Implments a new Plonk gadget
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Poiseidon Hash Implementation
7 participants