-
Notifications
You must be signed in to change notification settings - Fork 381
feat(test): Fuzz test stdlib hash functions #6233
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
Merged
Merged
Changes from 12 commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
854323e
Execute dummy test
aakoshh 91328aa
Dummy prop test
aakoshh 4737766
Proptest Keccak256
aakoshh 7f46d78
Factor out hash testing into a common method
aakoshh dc38d14
Test Sha256
aakoshh 68ffd58
Test Sha512
aakoshh 148c2f3
Test 100 cases instead of 256
aakoshh 24b9081
Remove regressions file
aakoshh 6e7e64b
Refactor variable length
aakoshh 2f6222f
Fix typos
aakoshh 2d6714f
Test to show that Keccak256 fails with large input
aakoshh dc3f3e9
chore: add additional lengths to cover #6163
TomAFrench facc16a
Move sha2 and sha3 to workspace
aakoshh b26cec6
Update tooling/nargo_cli/tests/stdlib-props.rs
aakoshh 8e976f4
Rename methods to fuzz_..._equivalence
aakoshh ac8b22a
Merge branch '6141-fuzz-stdlib' of github.com:noir-lang/noir into 614…
aakoshh File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # Seeds for failure cases proptest has generated in the past. It is | ||
| # automatically read and these particular cases re-run before any | ||
| # novel cases are generated. | ||
| # | ||
| # It is recommended to check this file in to source control so that | ||
| # everyone who runs the test benefits from these saved cases. | ||
| cc 88db0227d5547742f771c14b1679f6783570b46bf7cf9e6897ee1aca4bd5034d # shrinks to io = SnippetInputOutput { description: "force_brillig = false, max_len = 200", inputs: {"input": Vec([Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(1), Field(5), Field(20), Field(133), Field(233), Field(99), Field(2⁶), Field(196), Field(232), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0), Field(0)]), "message_size": Field(2⁶)}, expected_output: Vec([Field(102), Field(26), Field(94), Field(212), Field(102), Field(1), Field(215), Field(217), Field(167), Field(175), Field(158), Field(18), Field(20), Field(244), Field(158), Field(200), Field(2⁷), Field(186), Field(251), Field(243), Field(20), Field(207), Field(22), Field(3), Field(139), Field(81), Field(207), Field(2⁴), Field(50), Field(167), Field(1), Field(163)]) } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,263 @@ | ||
| use std::{cell::RefCell, collections::BTreeMap, path::Path}; | ||
|
|
||
| use acvm::{acir::native_types::WitnessStack, FieldElement}; | ||
| use nargo::{ | ||
| ops::{execute_program, DefaultForeignCallExecutor}, | ||
| parse_all, | ||
| }; | ||
| use noirc_abi::input_parser::InputValue; | ||
| use noirc_driver::{ | ||
| compile_main, file_manager_with_stdlib, prepare_crate, CompilationResult, CompileOptions, | ||
| CompiledProgram, CrateId, | ||
| }; | ||
| use noirc_frontend::hir::Context; | ||
| use proptest::prelude::*; | ||
| use sha3::Digest; | ||
|
|
||
| /// Inputs and expected output of a snippet encoded in ABI format. | ||
| #[derive(Debug)] | ||
| struct SnippetInputOutput { | ||
| description: String, | ||
| inputs: BTreeMap<String, InputValue>, | ||
| expected_output: InputValue, | ||
| } | ||
| impl SnippetInputOutput { | ||
| fn new(inputs: Vec<(&str, InputValue)>, output: InputValue) -> Self { | ||
| Self { | ||
| description: "".to_string(), | ||
| inputs: inputs.into_iter().map(|(k, v)| (k.to_string(), v)).collect(), | ||
| expected_output: output, | ||
| } | ||
| } | ||
|
|
||
| /// Attach some description to hint at the scenario we are testing. | ||
| fn with_description(mut self, description: String) -> Self { | ||
| self.description = description; | ||
| self | ||
| } | ||
| } | ||
|
|
||
| /// Prepare a code snippet. | ||
| fn prepare_snippet(source: String) -> (Context<'static, 'static>, CrateId) { | ||
| let root = Path::new(""); | ||
| let file_name = Path::new("main.nr"); | ||
| let mut file_manager = file_manager_with_stdlib(root); | ||
| file_manager.add_file_with_source(file_name, source).expect( | ||
| "Adding source buffer to file manager should never fail when file manager is empty", | ||
| ); | ||
| let parsed_files = parse_all(&file_manager); | ||
|
|
||
| let mut context = Context::new(file_manager, parsed_files); | ||
| let root_crate_id = prepare_crate(&mut context, file_name); | ||
|
|
||
| (context, root_crate_id) | ||
| } | ||
|
|
||
| /// Compile the main function in a code snippet. | ||
| /// | ||
| /// Use `force_brillig` to test it as an unconstrained function without having to change the code. | ||
| /// This is useful for methods that use the `runtime::is_unconstrained()` method to change their behavior. | ||
| fn prepare_and_compile_snippet( | ||
| source: String, | ||
| force_brillig: bool, | ||
| ) -> CompilationResult<CompiledProgram> { | ||
| let (mut context, root_crate_id) = prepare_snippet(source); | ||
| let options = CompileOptions { force_brillig, ..Default::default() }; | ||
| compile_main(&mut context, root_crate_id, &options, None) | ||
| } | ||
|
|
||
| /// Compile a snippet and run property tests against it by generating random input/output pairs | ||
| /// according to the strategy, executing the snippet with the input, and asserting that the | ||
| /// output it returns is the one we expect. | ||
| fn run_snippet_proptest( | ||
| source: String, | ||
| force_brillig: bool, | ||
| strategy: BoxedStrategy<SnippetInputOutput>, | ||
| ) { | ||
| let program = match prepare_and_compile_snippet(source.clone(), force_brillig) { | ||
| Ok((program, _)) => program, | ||
| Err(e) => panic!("failed to compile program:\n{source}\n{e:?}"), | ||
| }; | ||
|
|
||
| let blackbox_solver = bn254_blackbox_solver::Bn254BlackBoxSolver; | ||
| let foreign_call_executor = | ||
| RefCell::new(DefaultForeignCallExecutor::new(false, None, None, None)); | ||
|
|
||
| // Generate multiple input/output | ||
| proptest!(ProptestConfig::with_cases(100), |(io in strategy)| { | ||
| let initial_witness = program.abi.encode(&io.inputs, None).expect("failed to encode"); | ||
| let mut foreign_call_executor = foreign_call_executor.borrow_mut(); | ||
|
|
||
| let witness_stack: WitnessStack<FieldElement> = execute_program( | ||
| &program.program, | ||
| initial_witness, | ||
| &blackbox_solver, | ||
| &mut *foreign_call_executor, | ||
| ) | ||
| .expect("failed to execute"); | ||
|
|
||
| let main_witness = witness_stack.peek().expect("should have return value on witness stack"); | ||
| let main_witness = &main_witness.witness; | ||
|
|
||
| let (_, return_value) = program.abi.decode(main_witness).expect("failed to decode"); | ||
| let return_value = return_value.expect("should decode a return value"); | ||
|
|
||
| prop_assert_eq!(return_value, io.expected_output, "{}", io.description); | ||
| }); | ||
| } | ||
|
|
||
| /// Run property tests on a code snippet which is assumed to execute a hashing function with the following signature: | ||
| /// | ||
| /// ```ignore | ||
| /// fn main(input: [u8; {max_len}], message_size: u32) -> pub [u8; 32] | ||
| /// ``` | ||
| /// | ||
| /// The calls are executed with and without forcing brillig, because it seems common for hash functions to run different | ||
| /// code paths based on `runtime::is_unconstrained()`. | ||
| fn run_hash_proptest<const N: usize>( | ||
| // Different generic maximum input sizes to try. | ||
| max_lengths: &[usize], | ||
| // Some hash functions allow inputs which are less than the generic parameters, others don't. | ||
| variable_length: bool, | ||
| // Make the source code specialized for a given expected input size. | ||
| source: impl Fn(usize) -> String, | ||
| // Rust implementation of the hash function. | ||
| hash: fn(&[u8]) -> [u8; N], | ||
| ) { | ||
| for max_len in max_lengths { | ||
| let max_len = *max_len; | ||
| // The maximum length is used to pick the generic version of the method. | ||
| let source = source(max_len); | ||
| // Hash functions runs differently depending on whether the code is unconstrained or not. | ||
| for force_brillig in [false, true] { | ||
| let length_strategy = | ||
| if variable_length { (0..=max_len).boxed() } else { Just(max_len).boxed() }; | ||
| // The actual input length can be up to the maximum. | ||
| let strategy = length_strategy | ||
| .prop_flat_map(|len| prop::collection::vec(any::<u8>(), len)) | ||
| .prop_map(move |mut msg| { | ||
| // The output is the hash of the data as it is. | ||
| let output = hash(&msg); | ||
|
|
||
| // The input has to be padded to the maximum length. | ||
| let msg_size = msg.len(); | ||
| msg.resize(max_len, 0u8); | ||
|
|
||
| let mut inputs = vec![("input", bytes_input(&msg))]; | ||
|
|
||
| // Omit the `message_size` if the hash function doesn't support it. | ||
| if variable_length { | ||
| inputs.push(( | ||
| "message_size", | ||
| InputValue::Field(FieldElement::from(msg_size)), | ||
| )); | ||
| } | ||
|
|
||
| SnippetInputOutput::new(inputs, bytes_input(&output)).with_description(format!( | ||
| "force_brillig = {force_brillig}, max_len = {max_len}" | ||
| )) | ||
| }) | ||
| .boxed(); | ||
|
|
||
| run_snippet_proptest(source.clone(), force_brillig, strategy); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// This is just a simple test to check that property testing works. | ||
| #[test] | ||
| fn test_basic() { | ||
| let program = "fn main(init: u32) -> pub u32 { | ||
| let mut x = init; | ||
| for i in 0 .. 6 { | ||
| x += i; | ||
| } | ||
| x | ||
| }"; | ||
|
|
||
| let strategy = any::<u32>() | ||
| .prop_map(|init| { | ||
| let init = init / 2; | ||
| SnippetInputOutput::new( | ||
| vec![("init", InputValue::Field(init.into()))], | ||
| InputValue::Field((init + 15).into()), | ||
| ) | ||
| }) | ||
| .boxed(); | ||
|
|
||
| run_snippet_proptest(program.to_string(), false, strategy); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_keccak256() { | ||
| run_hash_proptest( | ||
| // XXX: Currently it fails with inputs >= 135 bytes | ||
| &[0, 1, 100, 134], | ||
| true, | ||
| |max_len| { | ||
| format!( | ||
| "fn main(input: [u8; {max_len}], message_size: u32) -> pub [u8; 32] {{ | ||
| std::hash::keccak256(input, message_size) | ||
| }}" | ||
| ) | ||
| }, | ||
| |data| sha3::Keccak256::digest(data).try_into().unwrap(), | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| #[should_panic] // Remove once fixed | ||
| fn test_keccak256_over_135() { | ||
| run_hash_proptest( | ||
| &[135, 150], | ||
| true, | ||
| |max_len| { | ||
| format!( | ||
| "fn main(input: [u8; {max_len}], message_size: u32) -> pub [u8; 32] {{ | ||
| std::hash::keccak256(input, message_size) | ||
| }}" | ||
| ) | ||
| }, | ||
| |data| sha3::Keccak256::digest(data).try_into().unwrap(), | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_sha256() { | ||
TomAFrench marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| run_hash_proptest( | ||
| &[0, 1, 200, 511, 512], | ||
| true, | ||
| |max_len| { | ||
| format!( | ||
| "fn main(input: [u8; {max_len}], message_size: u64) -> pub [u8; 32] {{ | ||
| std::hash::sha256_var(input, message_size) | ||
| }}" | ||
| ) | ||
| }, | ||
| // It's SHA2, not SHA3: | ||
| // |data| sha3::Sha3_256::digest(data).try_into().expect("result is 256 bits"), | ||
aakoshh marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| |data| sha2::Sha256::digest(data).try_into().unwrap(), | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_sha512() { | ||
| run_hash_proptest( | ||
| &[0, 1, 200], | ||
| false, | ||
| |max_len| { | ||
| format!( | ||
| "fn main(input: [u8; {max_len}]) -> pub [u8; 64] {{ | ||
| std::hash::sha512::digest(input) | ||
| }}" | ||
| ) | ||
| }, | ||
| |data| sha2::Sha512::digest(data).try_into().unwrap(), | ||
| ); | ||
| } | ||
|
|
||
| fn bytes_input(bytes: &[u8]) -> InputValue { | ||
| InputValue::Vec( | ||
| bytes.iter().map(|b| InputValue::Field(FieldElement::from(*b as u32))).collect(), | ||
| ) | ||
| } | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.