Skip to content

Commit

Permalink
feat: validate inputs of fn main (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
olehmisar authored Jan 27, 2025
1 parent f7fdec1 commit 7172080
Show file tree
Hide file tree
Showing 7 changed files with 276 additions and 7 deletions.
10 changes: 4 additions & 6 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
name: Noir tests

on:
push:
branches:
- main
pull_request:
on: [push]

env:
CARGO_TERM_COLOR: always
Expand All @@ -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
Expand Down
70 changes: 69 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
},
});
```
2 changes: 2 additions & 0 deletions src/lib.nr
Original file line number Diff line number Diff line change
Expand Up @@ -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<T, let N: u32> {
fn slice<let L: u32>(self, start: u32) -> [T; L];
Expand Down
99 changes: 99 additions & 0 deletions src/validate_inputs.nr
Original file line number Diff line number Diff line change
@@ -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<let N: u32> ValidateInput for str<N> {
fn validate(self) {}
}

impl<T, let N: u32> ValidateInput for [T; N]
where
T: ValidateInput,
{
fn validate(mut self) {
for i in 0..N {
self[i].validate();
}
}
}

impl<T, let MaxLen: u32> ValidateInput for BoundedVec<T, MaxLen>
where
T: ValidateInput,
{
fn validate(mut self) {
for i in 0..MaxLen {
if i < self.len() {
self.get_unchecked(i).validate()
}
}
}
}
6 changes: 6 additions & 0 deletions tests/Nargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[package]
name = "tests"
type = "lib"

[dependencies]
nodash = { path = "../" }
1 change: 1 addition & 0 deletions tests/src/lib.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mod validate_inputs;
95 changes: 95 additions & 0 deletions tests/src/validate_inputs.nr
Original file line number Diff line number Diff line change
@@ -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<U120, 2>) -> 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,
}

0 comments on commit 7172080

Please sign in to comment.