diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 80b420b..8a77759 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,10 +1,6 @@ name: Noir tests -on: - push: - branches: - - main - pull_request: +on: [push] env: CARGO_TERM_COLOR: always @@ -27,7 +23,9 @@ jobs: toolchain: ${{ matrix.toolchain }} - name: Run Noir tests - run: nargo test + run: | + nargo test + cd tests && nargo test format: runs-on: ubuntu-latest diff --git a/README.md b/README.md index 14032ed..9925d4d 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Nodash is a utility library for [Noir](https://github.com/noir-lang/noir) langua Put this into your Nargo.toml. ```toml -nodash = { git = "https://github.com/olehmisar/nodash/", tag = "v0.39.5" } +nodash = { git = "https://github.com/olehmisar/nodash/", tag = "v0.39.6" } ``` ## Docs @@ -201,3 +201,71 @@ use nodash::pack_bytes; let bytes: [u8; 32] = [0; 32]; let packed = pack_bytes(bytes); ``` + +### Validate `main` function inputs + +`fn main` inputs are not validated by Noir. For example, you have a `U120` struct like this: + +```rs +struct U120 { + inner: Field, +} + +impl U120 { + fn new(inner: Field) -> Self { + inner.assert_max_bit_size::<120>(); + Self { inner } + } +} +``` + +You then can create instances of `U120` with `U120::new(123)`. If you pass a value that is larger than 2^120 to `U120::new`, you will get a runtime error because we assert the max bit size of `Field` in `U120::new`. + +However, Noir does not check the validity of `U120` fields when passed to a `fn main` function. For example, for this circuit + +```rs +fn main(a: U120) { + // do something with a +} +``` + +...you can pass any arbitrary value to `a` from JavaScript and it will NOT fail in Noir when `main` is executed: + +```js +// this succeeds but it shouldn't! +await noir.execute({ + a: { + inner: 2n * 10n ** 120n + 1n, + }, +}); +``` + +To fix this, you can use the `validate_inputs` attribute on the `main` function: + +```rs +use nodash::{validate_inputs, ValidateInput}; + +// this attribute checks that `U120` is within the range via `ValidateInput` trait +#[validate_inputs] +fn main(a: U120) { + // do something with a +} + +impl ValidateInput for U120 { + fn validate(self) { + // call the `new` function that asserts the max bit size + U120::new(self.inner); + } +} +``` + +Now, if you pass a value that is larger than 2^120 to `a` in JavaScript, you will get a runtime error: + +```js +// runtime error: "Assertion failed: call to assert_max_bit_size" +await noir.execute({ + a: { + inner: 2n * 10n ** 120n + 1n, + }, +}); +``` diff --git a/src/lib.nr b/src/lib.nr index 3f569cd..ad37900 100644 --- a/src/lib.nr +++ b/src/lib.nr @@ -4,11 +4,13 @@ mod solidity; mod string; mod tables; mod array; +mod validate_inputs; pub use array::pack_bytes; pub use hash::{keccak256, pedersen, poseidon2, sha256}; pub use math::{clamp, div_ceil, sqrt::sqrt}; pub use string::{field_to_hex, ord, str_to_u64, to_hex_string_bytes}; +pub use validate_inputs::{validate_inputs, ValidateInput}; pub trait ArrayExtensions { fn slice(self, start: u32) -> [T; L]; diff --git a/src/validate_inputs.nr b/src/validate_inputs.nr new file mode 100644 index 0000000..09f081c --- /dev/null +++ b/src/validate_inputs.nr @@ -0,0 +1,99 @@ +pub comptime fn validate_inputs(f: FunctionDefinition) { + let validated_inputs = f + .parameters() + .map(|(name, _typ): (Quoted, Type)| quote {{ nodash::ValidateInput::validate($name); }}) + .join(quote {;}); + let checks_body = quote {{ $validated_inputs }}.as_expr().expect( + f"failed to parse ValidateInput checks code", + ); // should never fail + + let old_body = f.body(); + let checked_body = quote {{ + $checks_body; + $old_body + }}; + f.set_body(checked_body.as_expr().expect(f"failed to concatenate body with checks")); +} + +#[derive_via(derive_validate_input)] +pub trait ValidateInput { + fn validate(self); +} + +comptime fn derive_validate_input(s: StructDefinition) -> Quoted { + let name = quote { nodash::ValidateInput }; + let signature = quote { fn validate(self) }; + let for_each_field = |name| quote { nodash::ValidateInput::validate(self.$name); }; + let body = |fields| quote { $fields }; + std::meta::make_trait_impl(s, name, signature, for_each_field, quote { , }, body) +} + +impl ValidateInput for u8 { + fn validate(self) {} +} + +impl ValidateInput for u16 { + fn validate(self) {} +} + +impl ValidateInput for u32 { + fn validate(self) {} +} + +impl ValidateInput for u64 { + fn validate(self) {} +} + +impl ValidateInput for i8 { + fn validate(self) {} +} +impl ValidateInput for i16 { + fn validate(self) {} +} +impl ValidateInput for i32 { + fn validate(self) {} +} + +impl ValidateInput for i64 { + fn validate(self) {} +} + +impl ValidateInput for Field { + fn validate(self) {} +} + +impl ValidateInput for bool { + fn validate(self) {} +} + +impl ValidateInput for U128 { + fn validate(self) {} +} + +impl ValidateInput for str { + fn validate(self) {} +} + +impl ValidateInput for [T; N] +where + T: ValidateInput, +{ + fn validate(mut self) { + for i in 0..N { + self[i].validate(); + } + } +} + +impl ValidateInput for BoundedVec +where + T: ValidateInput, +{ + fn validate(mut self) { + for i in 0..MaxLen { + if i < self.len() { + self.get_unchecked(i).validate() + } + } + } +} diff --git a/tests/Nargo.toml b/tests/Nargo.toml new file mode 100644 index 0000000..8c13a6b --- /dev/null +++ b/tests/Nargo.toml @@ -0,0 +1,6 @@ +[package] +name = "tests" +type = "lib" + +[dependencies] +nodash = { path = "../" } diff --git a/tests/src/lib.nr b/tests/src/lib.nr new file mode 100644 index 0000000..06869ba --- /dev/null +++ b/tests/src/lib.nr @@ -0,0 +1 @@ +mod validate_inputs; diff --git a/tests/src/validate_inputs.nr b/tests/src/validate_inputs.nr new file mode 100644 index 0000000..5be7c14 --- /dev/null +++ b/tests/src/validate_inputs.nr @@ -0,0 +1,95 @@ +#[nodash::validate_inputs] +fn my_main(a: Field, b: u64) -> Field { + a + b as Field +} + +#[test] +fn test_validate_inputs() { + let result = my_main(1, 2); + assert(result == 3); +} + +#[nodash::validate_inputs] +fn main_collections(a: [U120; 1], b: BoundedVec) -> Field { + a[0].inner + b.get(0).inner +} + +#[test] +fn test_validate_collections() { + let result = main_collections( + [U120::new(1)], + BoundedVec::from_parts([U120::new(2), U120 { inner: 2.pow_32(120) }], 1), + ); + assert(result == 3); +} + +#[test(should_fail_with = "call to assert_max_bit_size")] +fn test_validate_array_fail() { + let _ = main_collections([U120 { inner: 2.pow_32(120) }], BoundedVec::new()); +} + +#[test(should_fail_with = "call to assert_max_bit_size")] +fn test_validate_bounded_vec_fail() { + let _ = main_collections( + [U120::new(1)], + BoundedVec::from_parts([U120::new(2), U120 { inner: 2.pow_32(120) }], 2), + ); +} + +#[nodash::validate_inputs] +fn main_u120(a: U120) -> Field { + a.inner +} + +#[test] +fn test_validate_u120() { + let inner = 2.pow_32(120) - 1; + let result = main_u120(U120 { inner }); + assert(result == inner); +} + +#[test(should_fail_with = "call to assert_max_bit_size")] +fn test_validate_u120_fail() { + let inner = 2.pow_32(120); + let _ = main_u120(U120 { inner }); +} + +#[nodash::validate_inputs] +fn main_struct_derive(a: NestedStruct) -> Field { + a.value.inner +} + +#[test] +fn test_validate_struct_derive() { + let inner = 2.pow_32(120) - 1; + let result = main_struct_derive(NestedStruct { value: U120 { inner } }); + assert(result == inner); +} + +#[test(should_fail_with = "call to assert_max_bit_size")] +fn test_validate_struct_derive_fail() { + let inner = 2.pow_32(120); + let _ = main_struct_derive(NestedStruct { value: U120 { inner } }); +} + +struct U120 { + inner: Field, +} + +impl U120 { + fn new(inner: Field) -> Self { + inner.assert_max_bit_size::<120>(); + Self { inner } + } +} + +impl nodash::ValidateInput for U120 { + fn validate(self) { + let _ = U120::new(self.inner); + } +} + +#[derive(nodash::ValidateInput)] +struct NestedStruct { + value: U120, +}