Skip to content

Commit

Permalink
feat(test): Fuzz test poseidon2 hash equivalence (#6265)
Browse files Browse the repository at this point in the history
# Description

## Problem\*

Related to #6141 

## Summary\*

Adds a fuzz test for `poseidon2.nr` comparing the results to
[bn254_blackbox_solver::poseidon_hash](https://github.com/noir-lang/noir/blob/70cbeb4322a0b11c1c167ab27bf0408d04fe7b7d/acvm-repo/bn254_blackbox_solver/src/poseidon2.rs#L547),
which says it's `"Performs a poseidon hash with a sponge construction
equivalent to the one in poseidon2.nr"`

To pass the test the Rust implementation was given a new
`is_variable_length` parameter to inform it whether it needs to append
an extra 1 like Noir and Berratenberg do.

## Additional Context

The test initially failed:

```console
❯ cargo test -p nargo_cli --test stdlib-props poseidon
  
test fuzz_poseidon2_equivalence ... FAILED

---- fuzz_poseidon2_equivalence stdout ----
Test failed: assertion failed: `(left == right)` 
  left: `Field(1187863985434533916290764679013201786939267142671550539990974992402592744116)`, 
 right: `Field(11250791130336988991462250958918728798886439319225016858543557054782819955502)`: max_len = 1 at tooling/nargo_cli/tests/stdlib-props.rs:106.
minimal failing input: io = SnippetInputOutput {
    description: "max_len = 1",
    inputs: {
        "input": Vec(
            [
                Field(
                    0,
                ),
            ],
        ),
        "message_size": Field(
            0,
        ),
    },
    expected_output: Field(
        11250791130336988991462250958918728798886439319225016858543557054782819955502,
    ),
}
```

So we pass in `input = [0; 1]` with `message_size=0`. It fails because
the Noir code treats the case where the `message_size` is different from
the maximum length differently by
[appending](https://github.com/noir-lang/noir/blob/70cbeb4322a0b11c1c167ab27bf0408d04fe7b7d/noir_stdlib/src/hash/poseidon2.nr#L75-L80)
an extra 1, to keep variable and fixed length hashes distinct. The Rust
implementation doesn't do this, nor did the other hashes tested so far.
I'm not sure if it's worth noting that the hash will not depend on how
much shorter the message is than the maximum, just that it's shorter.


## Documentation\*

Check one:
- [ ] No documentation needed.
- [ ] Documentation included in this PR.
- [ ] **[For Experimental Features]** Documentation to be submitted in a
separate PR.

# PR Checklist\*

- [x] I have tested the changes locally.
- [ ] I have formatted the changes with [Prettier](https://prettier.io/)
and/or `cargo fmt` on default settings.
  • Loading branch information
aakoshh authored Oct 10, 2024
1 parent 1cd2587 commit f61ba03
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 3 deletions.
14 changes: 12 additions & 2 deletions acvm-repo/bn254_blackbox_solver/src/poseidon2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -544,13 +544,23 @@ impl<'a> Poseidon2<'a> {
}

/// Performs a poseidon hash with a sponge construction equivalent to the one in poseidon2.nr
pub fn poseidon_hash(inputs: &[FieldElement]) -> Result<FieldElement, BlackBoxResolutionError> {
///
/// The `is_variable_length` parameter is there to so we can produce an equivalent hash with
/// the Barretenberg implementation which distinguishes between variable and fixed length inputs.
/// Set it to true if the input length matches the static size expected by the Noir function.
pub fn poseidon_hash(
inputs: &[FieldElement],
is_variable_length: bool,
) -> Result<FieldElement, BlackBoxResolutionError> {
let two_pow_64 = 18446744073709551616_u128.into();
let iv = FieldElement::from(inputs.len()) * two_pow_64;
let mut sponge = Poseidon2Sponge::new(iv, 3);
for input in inputs.iter() {
sponge.absorb(*input)?;
}
if is_variable_length {
sponge.absorb(FieldElement::from(1u32))?;
}
sponge.squeeze()
}

Expand Down Expand Up @@ -640,7 +650,7 @@ mod test {
FieldElement::from(3u128),
FieldElement::from(4u128),
];
let result = super::poseidon_hash(&fields).expect("should hash successfully");
let result = super::poseidon_hash(&fields, false).expect("should hash successfully");
assert_eq!(
result,
field_from_hex("130bf204a32cac1f0ace56c78b731aa3809f06df2731ebcf6b3464a15788b1b9"),
Expand Down
44 changes: 43 additions & 1 deletion tooling/nargo_cli/tests/stdlib-props.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::{cell::RefCell, collections::BTreeMap, path::Path};

use acvm::{acir::native_types::WitnessStack, FieldElement};
use acvm::{acir::native_types::WitnessStack, AcirField, FieldElement};
use iter_extended::vecmap;
use nargo::{
ops::{execute_program, DefaultForeignCallExecutor},
parse_all,
Expand Down Expand Up @@ -254,6 +255,47 @@ fn fuzz_sha512_equivalence() {
);
}

#[test]
fn fuzz_poseidon2_equivalence() {
use bn254_blackbox_solver::poseidon_hash;

for max_len in [0, 1, 3, 4, 511, 512] {
let source = format!(
"fn main(input: [Field; {max_len}], message_size: u32) -> pub Field {{
std::hash::poseidon2::Poseidon2::hash(input, message_size)
}}"
);

let strategy = (0..=max_len)
.prop_flat_map(|len: usize| {
// Generate Field elements from random 32 byte vectors.
let field = prop::collection::vec(any::<u8>(), 32)
.prop_map(|bytes| FieldElement::from_be_bytes_reduce(&bytes));

prop::collection::vec(field, len)
})
.prop_map(move |mut msg| {
// The output hash is a single field element.
let output = poseidon_hash(&msg, msg.len() < max_len).expect("failed to hash");

// The input has to be padded to the maximum length.
let msg_size = msg.len();
msg.resize(max_len, FieldElement::from(0u64));

let inputs = vec![
("input", InputValue::Vec(vecmap(msg, InputValue::Field))),
("message_size", InputValue::Field(FieldElement::from(msg_size))),
];

SnippetInputOutput::new(inputs, InputValue::Field(output))
.with_description(format!("max_len = {max_len}"))
})
.boxed();

run_snippet_proptest(source.clone(), false, strategy);
}
}

fn bytes_input(bytes: &[u8]) -> InputValue {
InputValue::Vec(
bytes.iter().map(|b| InputValue::Field(FieldElement::from(*b as u32))).collect(),
Expand Down

0 comments on commit f61ba03

Please sign in to comment.