Skip to content

Commit

Permalink
feat(stdlib): Implement Poseidon hash (#768)
Browse files Browse the repository at this point in the history
* Implement Poseidon hash

* Minor changes. Addition of BN254-specific permutation and sponge functions.

* Stylistic changes

* Minor changes. Exclude sponge test.

* Replace stdlib functions with methods

* Fix typo

* Fix typo
  • Loading branch information
ax0 authored Apr 3, 2023
1 parent 98c2cd9 commit 779ab66
Show file tree
Hide file tree
Showing 12 changed files with 589 additions and 3 deletions.
6 changes: 6 additions & 0 deletions crates/nargo/tests/test_data/poseidonperm_x5_254/Nargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[package]
name = "Poseidon 254-bit permutation test on 3 elements with alpha = 5"
authors = [""]
compiler_version = "0.1"

[dependencies]
4 changes: 4 additions & 0 deletions crates/nargo/tests/test_data/poseidonperm_x5_254/Prover.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
x1 = [0,1,2]
y1 = "0x115cc0f5e7d690413df64c6b9662e9cf2a3617f2743245519e19607a4417189a"
x2 = [0,1,2,3,4]
y2 = "0x299c867db6c1fdd79dcefa40e4510b9837e60ebb1ce0663dbaa525df65250465"
10 changes: 10 additions & 0 deletions crates/nargo/tests/test_data/poseidonperm_x5_254/src/main.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use dep::std::hash::poseidon;

fn main(x1: [Field; 3], y1: pub Field, x2: [Field; 5], y2: pub Field)
{
let perm1 = poseidon::bn254::perm::x5_3(x1);
constrain perm1[0] == y1;

let perm2 = poseidon::bn254::perm::x5_5(x2);
constrain perm2[0] == y2;
}
6 changes: 6 additions & 0 deletions crates/nargo/tests/test_data/poseidonsponge_x5_254/Nargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[package]
name = "Variable-length Poseidon-128 sponge test on 7 elements with alpha = 5"
authors = [""]
compiler_version = "0.1"

[dependencies]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
x = [1,2,3,4,5,6,7]
14 changes: 14 additions & 0 deletions crates/nargo/tests/test_data/poseidonsponge_x5_254/src/main.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
use dep::std::hash::poseidon;

fn main(x: [Field; 7])
{
// Test optimised sponge
let result = poseidon::bn254::sponge(x);

constrain result == 0x080ae1669d62f0197190573d4a325bfb8d8fc201ce3127cbac0c47a7ac81ac48;

// Test unoptimised sponge
let result2 = poseidon::absorb(poseidon::bn254::consts::x5_5_config(), [0;5], 4, 1, x)[1];

constrain result2 == result;
}
6 changes: 3 additions & 3 deletions crates/nargo_cli/tests/test_data/config.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# List of tests to be excluded (i.e not run), as their directory name in test_data
# "1_mul", "2_div","3_add","4_sub","5_over", "6","6_array", "7_function","7","8_integration", "9_conditional", "10_slices", "assign_ex", "bool_not", "bool_or", "pedersen_check", "pred_eq", "schnorr", "sha256", "tuples",
# "1_mul", "2_div","3_add","4_sub","5_over", "6","6_array", "7_function","7","8_integration", "9_conditional", "10_slices", "assign_ex", "bool_not", "bool_or", "pedersen_check", "poseidonperm_x5_254", "poseidonsponge_x5_254", "pred_eq", "schnorr", "sha256", "tuples",
# "array_len", "array_neq", "bit_and", "cast_bool", "comptime_array_access", "generics", "global_comptime", "main_bool_arg", "main_return", "merkle_insert", "modules", "modules_more", "scalar_mul", "simple_shield", "struct", "submodules",
# Exclude "sha2_byte" due to relatively long computation time and "sha2_blocks" due to very long computation time.
exclude = ["comptime_fail", "sha2_blocks", "sha2_byte"]
# Exclude "poseidonsponge_x5_254" and "sha2_byte" due to relatively long computation time and "sha2_blocks" due to very long computation time.
exclude = ["comptime_fail", "poseidonsponge_x5_254", "sha2_blocks", "sha2_byte"]


# List of tests (as their directory name in test_data) expecting to fail: if the test pass, we report an error.
Expand Down
1 change: 1 addition & 0 deletions noir_stdlib/src/hash.nr
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod poseidon;

#[foreign(sha256)]
fn sha256<N>(_input : [u8; N]) -> [u8; 32] {}
Expand Down
115 changes: 115 additions & 0 deletions noir_stdlib/src/hash/poseidon.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
mod bn254; // Instantiations of Poseidon for prime field of the same order as BN254

use crate::field::modulus_num_bits;

struct PoseidonConfig<M,N> {
t: comptime Field, // Width, i.e. state size
rf: comptime u8, // Number of full rounds; should be even
rp: comptime u8, // Number of partial rounds
alpha: comptime Field, // S-box power; depends on the underlying field
ark: [Field; M], // Additive round keys
mds: [Field; N] // MDS Matrix in row-major order
}

fn config<M,N>(
t: comptime Field,
rf: comptime u8,
rp: comptime u8,
alpha: comptime Field,
ark: [Field; M],
mds: [Field; N])
-> PoseidonConfig<M,N> {
// Input checks
constrain t as u8 * (rf + rp) == ark.len() as u8;
constrain t * t == mds.len();
constrain alpha != 0;

PoseidonConfig {t, rf, rp, alpha, ark, mds}
}

// General Poseidon permutation on elements of type Field
fn permute<M,N,O>(
pos_conf: PoseidonConfig<M, N>,
mut state: [Field; O])
-> [Field; O] {
let PoseidonConfig {t, rf, rp, alpha, ark, mds} = pos_conf;

constrain t == state.len();

let mut count = 0;

// for r in 0..rf + rp
for r in 0..(ark.len()/state.len()) {
for i in 0..state.len() {
state[i] = state[i] + ark[count + i];
} // Shift by round constants

state[0] = state[0].pow_32(alpha);

// Check whether we are in a full round
if (r as u8 < rf/2) | (r as u8 >= rf/2 + rp) {
for i in 1..state.len() {
state[i] = state[i].pow_32(alpha);
}
}

state = apply_matrix(mds, state); // Apply MDS matrix
count = count + t;
}

state
}

// Absorption. Fully absorbs input message.
fn absorb<M,N,O,P>(
pos_conf: PoseidonConfig<M, N>,
mut state: [Field; O], // Initial state; usually [0; O]
rate: comptime Field, // Rate
capacity: comptime Field, // Capacity; usually 1
msg: [Field; P]) // Arbitrary length message
-> [Field; O] {
constrain pos_conf.t == rate + capacity;

let mut i = 0;

for k in 0..msg.len() {
// Add current block to state
state[capacity + i] += msg[k];
i = i+1;

// Enough to absorb
if i == rate {
state = permute(pos_conf, state);
i = 0;
}
}

// If we have one more block to permute
if i != 0 {
state = permute(pos_conf, state);
}

state
}


// Check security of sponge instantiation
fn check_security(rate: Field, width: Field, security: Field) -> bool {
let n = modulus_num_bits();

((n-1)*(width-rate)/2) as u8 > security as u8
}

// A*x where A is an n x n matrix in row-major order and x an n-vector
fn apply_matrix<N>(a: [Field], x: [Field; N]) -> [Field; N] {
let mut y = x;

for i in 0..x.len() {
y[i] = 0;
for j in 0..x.len() {
y[i] = y[i] + a[x.len()*i + j]* x[j];
}
}

y
}
103 changes: 103 additions & 0 deletions noir_stdlib/src/hash/poseidon/bn254.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Instantiations of Poseidon constants, permutations and sponge for prime field of the same order as BN254
mod perm;
mod consts;

use crate::hash::poseidon::PoseidonConfig;
use crate::hash::poseidon::apply_matrix;

// Optimised permutation for this particular field; uses hardcoded rf and rp values,
// which should agree with those in pos_conf.
fn permute<M,N,O>(
pos_conf: PoseidonConfig<M, N>,
mut state: [Field; O])
-> [Field; O] {
let PoseidonConfig {t, rf: config_rf, rp: config_rp, alpha, ark, mds} = pos_conf;
let rf = 8;
let rp = [56, 57, 56, 60, 60, 63, 64, 63, 60, 66, 60, 65, 70, 60, 64, 68][state.len() - 2];

constrain t == state.len();
constrain rf == config_rf as Field;
constrain rp == config_rp as Field;

let mut count = 0;

// First half of full rounds
for _r in 0..rf/2 {
for i in 0..state.len() {
state[i] = state[i] + ark[count + i];
} // Shift by round constants

for i in 0..state.len() {
state[i] = state[i].pow_32(alpha);
}

state = apply_matrix(mds, state); // Apply MDS matrix
count = count + t;
}

// Partial rounds
for _r in 0..rp {
for i in 0..state.len() {
state[i] = state[i] + ark[count + i];
} // Shift by round constants

state[0] = state[0].pow_32(alpha);

state = apply_matrix(mds, state); // Apply MDS matrix
count = count + t;
}

// Second half of full rounds
for _r in 0..rf/2 {
for i in 0..state.len() {
state[i] = state[i] + ark[count + i];
} // Shift by round constants

for i in 0..state.len() {
state[i] = state[i].pow_32(alpha);
}

state = apply_matrix(mds, state); // Apply MDS matrix
count = count + t;
}

state
}

// Corresponding absorption.
fn absorb<M,N,O,P>(
pos_conf: PoseidonConfig<M, N>,
mut state: [Field; O], // Initial state; usually [0; O]
rate: comptime Field, // Rate
capacity: comptime Field, // Capacity; usually 1
msg: [Field; P] // Arbitrary length message
) -> [Field; O] {

constrain pos_conf.t == rate + capacity;

let mut i = 0;

for k in 0..msg.len() {
// Add current block to state
state[capacity + i] += msg[k];
i = i+1;

// Enough to absorb
if i == rate {
state = permute(pos_conf, state);
i = 0;
}
}

// If we have one more block to permute
if i != 0 {
state = permute(pos_conf, state);
}

state
}

// Variable-length Poseidon-128 sponge as suggested in second bullet point of §3 of https://eprint.iacr.org/2019/458.pdf
fn sponge<N>(msg: [Field; N]) -> Field {
absorb(consts::x5_5_config(), [0;5], 4, 1, msg)[1]
}
194 changes: 194 additions & 0 deletions noir_stdlib/src/hash/poseidon/bn254/consts.nr

Large diffs are not rendered by default.

Loading

0 comments on commit 779ab66

Please sign in to comment.