-
Notifications
You must be signed in to change notification settings - Fork 76
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
base: master
Are you sure you want to change the base?
Poseidon hash #114
Conversation
Is this PR ready for review @GhostOfGauss? If so, please convert from draft. |
No, not ready yet |
plonk-hashing/src/lib.rs
Outdated
#[macro_use] | ||
pub extern crate ark_std; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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 = ()> { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
// 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can use const-generics. See this example: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=cc008c861ad5c5455d1ca26622284a4b.
There was a problem hiding this comment.
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"
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this 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. |
There was a problem hiding this comment.
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
// 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; |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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] |
There was a problem hiding this comment.
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::{ |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use F::from_le_bytes_mod_order
instead of while
loop here. See https://github.com/arkworks-rs/sponge/blob/51d6fc9aac1fa69f44a04839202b5de828584ed8/src/poseidon/grain_lfsr.rs#L135
@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? |
There was a problem hiding this 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 { |
There was a problem hiding this comment.
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(), |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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);
There was a problem hiding this comment.
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`). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// `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>, |
There was a problem hiding this comment.
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>, |
There was a problem hiding this comment.
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).
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