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 hashing #11

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion builtins/src/ec_op/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::ecdsa::DoublingStep;
use crate::ecdsa::EcMadPartialStep;
use ark_ec::short_weierstrass::Affine;
use ark_ec::short_weierstrass::SWCurveConfig;
use ark_ec::short_weierstrass::Projective;
use ark_ec::short_weierstrass::Projective;
use ark_ec::CurveGroup;
use ark_ec::Group;
use binary::EcOpInstance;
Expand Down
133 changes: 132 additions & 1 deletion builtins/src/poseidon/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,70 @@ use crate::poseidon::params::PARTIAL_ROUND_KEYS_OPTIMIZED;
use crate::utils::Mat3x3;
use ark_ff::Field;
use num_bigint::BigUint;
use ruint::aliases::U256;

pub fn poseidon_hash_single(x: Fp) -> Fp {
let instance = PoseidonInstance {
index: 0,
input0: U256::from_limbs(x.0 .0),
input1: U256::from(0),
input2: U256::from(0),
Copy link
Owner

Choose a reason for hiding this comment

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

Might need to set input2 to 1

"the capacity element is initialized to 1"

};
let result = InstanceTrace::new(instance);
Copy link
Owner

Choose a reason for hiding this comment

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

Using InstanceTrace will decrease performance since it is not optimised for speed but rather obtaining the values needed in the STARK trace.

I think this PR should implement a seperate poseidon hash function that's focussed more on performance (i.e. doesn't keep track of the state of rounds in the permutation). Let's move all the added methods and tests in this file to crypto/src/hash/poseidon.rs. I'd suggest using the parameters from builtins/src/poseidon/params.rs and using InstanceTrace::new as a guide. Let's put the implementation in a new function called poseidon_permutation([Fp; 3]).


result.output0
}

pub fn poseidon_hash(x: Fp, y: Fp) -> Fp {
let instance = PoseidonInstance {
index: 0,
input0: U256::from_limbs(x.0 .0),
input1: U256::from_limbs(y.0 .0),
input2: U256::from(0),
Copy link
Owner

Choose a reason for hiding this comment

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

};
let result = InstanceTrace::new(instance);

result.output0
}

pub fn poseidon_hash_many(elements: Vec<Fp>) -> Fp {
let instance = PoseidonInstance {
index: 0,
input0: U256::from(0),
input1: U256::from(0),
input2: U256::from(0),
};
let mut state = InstanceTrace::new(instance);

let mut i = 0;
loop {
if i == elements.len() {
let result = InstanceTrace::new(PoseidonInstance {
index: state.instance.index,
input0: U256::from_limbs((state.output0 + Fp::from(1)).0 .0),
input1: U256::from_limbs(state.output1.0 .0),
input2: U256::from_limbs(state.output2.0 .0),
});
return result.output0;
} else if i == elements.len() - 1 {
let result = InstanceTrace::new(PoseidonInstance {
index: state.instance.index,
input0: U256::from_limbs((state.output0 + elements[i]).0 .0),
input1: U256::from_limbs((state.output1 + Fp::from(1)).0 .0),
input2: U256::from_limbs(state.output2.0 .0),
});
return result.output0;
} else {
state = InstanceTrace::new(PoseidonInstance {
index: state.instance.index,
input0: U256::from_limbs((state.output0 + elements[i]).0 .0),
input1: U256::from_limbs((state.output1 + elements[i + 1]).0 .0),
input2: U256::from_limbs(state.output2.0 .0),
});
}
i += 2;
}
}

/// Stores the states within a full round
#[derive(Clone, Copy, Debug)]
Expand Down Expand Up @@ -243,7 +307,8 @@ fn _calc_optimized_partial_round_keys() -> [[Fp; 3]; NUM_PARTIAL_ROUNDS] {

#[cfg(test)]
mod tests {
use crate::poseidon::permute;
use crate::poseidon::{permute, poseidon_hash_many};
use ark_ff::BigInt;
use ark_ff::MontFp as Fp;
use ark_ff::Field;
use ministark_gpu::fields::p3618502788666131213697322783095070105623107215331596699973092056135872020481::ark::Fp;
Expand All @@ -259,4 +324,70 @@ mod tests {

assert_eq!(expected, permute([Fp::ZERO, Fp::ZERO, Fp::ZERO]));
}

#[test]
fn poseidon_hash_many_3_example() {
Copy link
Owner

Choose a reason for hiding this comment

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

test name can just be "hash_3_values". Let's update the other test names as well. Also we should have tests for hash_single and poseidon_hash

let elements = vec![Fp!("0"), Fp!("0"), Fp!("0")];

let expected = BigInt::new([
14465169880788163794,
3725699649495491964,
13957675534445258432,
241034705846384105,
]);

assert_eq!(expected, poseidon_hash_many(elements).0);
}

#[test]
fn poseidon_hash_many_4_example() {
let elements = vec![Fp!("0"), Fp!("0"), Fp!("0"), Fp!("0")];

let expected = BigInt::new([
Copy link
Owner

Choose a reason for hiding this comment

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

How are all the expected values obtained?

8177194887955547932,
6869494578799689646,
3880460167861838970,
331313552213051804,
]);

assert_eq!(expected, poseidon_hash_many(elements).0);
}

#[test]
fn poseidon_hash_many_5_example() {
let elements = vec![Fp!("0"), Fp!("0"), Fp!("0"), Fp!("0"), Fp!("0")];

let expected = BigInt::new([
14872330746557636911,
10787657887407825984,
8559225264217750252,
304103888309894648,
]);

assert_eq!(expected, poseidon_hash_many(elements).0);
}

#[test]
fn poseidon_hash_many_9_example() {
let elements = vec![
Fp!("0"),
Fp!("0"),
Fp!("0"),
Fp!("0"),
Fp!("0"),
Fp!("0"),
Fp!("0"),
Fp!("0"),
Fp!("0"),
];

let expected = BigInt::new([
6218683884595264045,
16033324973008454317,
15358347862863079556,
557108717180864526,
]);

assert_eq!(expected, poseidon_hash_many(elements).0);
}
}
1 change: 1 addition & 0 deletions crypto/src/hash/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod blake2s;
pub mod keccak;
pub mod pedersen;
pub mod poseidon;

#[inline]
pub fn mask_least_significant_bytes<const N_UNMASKED_BYTES: u32>(bytes: &mut [u8]) {
Expand Down
74 changes: 74 additions & 0 deletions crypto/src/hash/poseidon.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
use std::fmt::Display;
use std::ops::Deref;
use std::iter::Iterator;
use ark_serialize::CanonicalDeserialize;
use ark_serialize::CanonicalSerialize;
use builtins::poseidon::poseidon_hash_many;
use digest::HashMarker;
use ministark::hash::Digest;
use ministark::hash::ElementHashFn;
use ministark::hash::HashFn;
use ministark_gpu::fields::p3618502788666131213697322783095070105623107215331596699973092056135872020481::ark::Fp;
use num_bigint::BigUint;
use ruint::aliases::U256;

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, CanonicalDeserialize, CanonicalSerialize)]
pub struct PoseidonDigest(pub Fp);

impl HashMarker for PoseidonDigest {}
Copy link
Owner

Choose a reason for hiding this comment

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

I think this can be removed


impl Display for PoseidonDigest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}

impl Digest for PoseidonDigest {
fn as_bytes(&self) -> [u8; 32] {
let num = U256::from(BigUint::from(self.0));
num.to_be_bytes::<32>()
}
}

impl Deref for PoseidonDigest {
type Target = Fp;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl From<Fp> for PoseidonDigest {
fn from(value: Fp) -> Self {
PoseidonDigest(value)
}
}

pub struct PoseidonHashFn;

impl HashFn for PoseidonHashFn {
type Digest = PoseidonDigest;
const COLLISION_RESISTANCE: u32 = 125;

fn hash(_bytes: impl IntoIterator<Item = u8>) -> PoseidonDigest {
unreachable!()
}

fn hash_chunks<'a>(_chunks: impl IntoIterator<Item = &'a [u8]>) -> Self::Digest {
unreachable!()
}

fn merge(v0: &PoseidonDigest, v1: &PoseidonDigest) -> PoseidonDigest {
PoseidonDigest(poseidon_hash_many([**v0, **v1].to_vec()))
Copy link
Owner

Choose a reason for hiding this comment

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

Due to how frequently this function is called it has a big impact on the overall performance of the prover. Allocations on the heap every invocation makes this function much more expensive than it needs to be. Let's implement this without any Vec

}

fn merge_with_int(seed: &PoseidonDigest, value: u64) -> PoseidonDigest {
PoseidonDigest(poseidon_hash_many([**seed, value.into()].to_vec()))
}
Comment on lines +65 to +67
Copy link
Owner

Choose a reason for hiding this comment

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

Suggested change
fn merge_with_int(seed: &PoseidonDigest, value: u64) -> PoseidonDigest {
PoseidonDigest(poseidon_hash_many([**seed, value.into()].to_vec()))
}
fn merge_with_int(seed: &PoseidonDigest, value: u64) -> PoseidonDigest {
self.merge(seed.0, value.into())
}

}

impl ElementHashFn<Fp> for PoseidonHashFn {
fn hash_elements(elements: impl IntoIterator<Item = Fp>) -> PoseidonDigest {
PoseidonDigest(poseidon_hash_many(elements.into_iter().collect()))
}
Comment on lines +71 to +73
Copy link
Owner

Choose a reason for hiding this comment

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

Let's make this function behave the same way as poseidon_hash_many_given_poseidon_perm and use the same padding rule

}
Loading