Skip to content
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

feat!: Wrapped unconstrained #6751

Closed
wants to merge 11 commits into from
Closed

feat!: Wrapped unconstrained #6751

wants to merge 11 commits into from

Conversation

Thunkar
Copy link
Contributor

@Thunkar Thunkar commented May 29, 2024

WIP (only for assessment by the Noir team)

Motivation

Best described by @TomAFrench in noir-lang/noir#4442. This is an alternative approach to solving that problem, using a combination of #6618 and a little bit of extra support by the language at the frontend level.

Overview

The implementation is based on the following components:

  • Unconstrained<T>: This PR adds an Unconstrained<T> type that manifests as a special UnresolvedTypeData. This pseudotype gets converted into a regular Struct type in the resolver, that is provided by the stdlib. Adding the type allows a very clean interface that adds a new validation rule to the parser: unconstrained functions must always return Unconstrained<T> or nothing at all. This type exposes two basic methods: .unwrap() to extract the inner value much like Option<T> and make_constrained(constrainer), which invites the user to assert the returned values and potentially map them to a "safe" version.
  • New type coercion: Much like .as_slice(), return values from unconstrained functions are automatically coerced to Unconstrained<T>, avoiding a lot of boilerplate
  • New intrinsic function assert_unconstrained. This forbids the user from using .unwrap() in a constrained environment,
    which ensures they have at least attempted to make the value secure (or makes very obvious they didn't).

UX

There's a great example of usage in the standard library

// Decomposes a single field into two 16 byte fields.
fn compute_decomposition(x: Field) -> (Field, Field) {
    let x_bytes = x.to_le_bytes(32);

    let mut low: Field = 0;
    let mut high: Field = 0;

    let mut offset = 1;
    for i in 0..16 {
        low += (x_bytes[i] as Field) * offset;
        high += (x_bytes[i + 16] as Field) * offset;
        offset *= 256;
    }

    (low, high)
}

unconstrained fn decompose_hint(x: Field) -> Unconstrained<(Field, Field)> {
    compute_decomposition(x)
}

/// Decompose a single field into two 16 byte fields.
pub fn decompose(x: Field) -> (Field, Field) {
    if is_unconstrained() {
        compute_decomposition(x)
    } else {
        // Take hints of the decomposition
        decompose_hint(x).make_constrained(
            | (xlo, xhi): (Field, Field) | {
                // Range check the limbs
                xlo.assert_max_bit_size(128);
                xhi.assert_max_bit_size(128);

                // Check that the decomposition is correct
                assert_eq(x, xlo + TWO_POW_128 * xhi);

                // Assert that the decomposition of P is greater than the decomposition of x
                assert_gt_limbs((PLO, PHI), (xlo, xhi));
                (xlo, xhi)
            }
        )
    }
}

Also meaningful compilation errors are returned with this approach:

Trying to return a regular old value from an unconstrained function:
image

Trying to .unwrap() an unconstrained value from a constrained function:
image

.unwrap() being used in a nested unconstrained call:
image

Problems found

  • Implementing Unconstrained<T> as both a type and an identifier (much like Field) made Cannot access trait type methods from built-in types. noir-lang/noir#4463 show its ugly head again. I modified the .ident() parser to accept certain keywords as valid idents, which I think could also be a solution to the aforementioned bug (and doesn't seem to cause any unintended side effects)
  • Type coercion (the only one that exists .as_slice(), but I used the same approach for mine) was broken on entrypoint (or main) functions. The expr_id of the body of a function was being replaced by that of a call, which failed in a later stage of the compiler. I fixed it by wrapping the conversion in a new block.
  • The type coercion involving a function of a generic struct was a nightmare and forced me to do a sort of poor's man monomorphization. There are probably way better ways of doing it, but my understanding of the internals of the compiler is limited

TODOs

  • Get the seal of approval ™️ from the magnificent creatures in the Noir team
  • Get back from vacation on Monday
  • Fix tests in noir repo
  • Integrate in aztec-packages. (sort of done, I need to run e2e, but I migrated all of aztec-nr to this new system to ensure it worked and was usable...it's in another branch because these changes probably belong in the noir repo and should be moved there)

@Thunkar Thunkar self-assigned this May 29, 2024
@Thunkar Thunkar requested a review from TomAFrench May 29, 2024 20:14
Comment on lines 1 to +24
#[builtin(is_unconstrained)]
pub fn is_unconstrained() -> bool {}

#[builtin(assert_unconstrained)]
pub fn assert_unconstrained() {}

struct Unconstrained<T> {
_value: T,
}

impl<T> Unconstrained<T> {
fn new(value: T) -> Self {
Self { _value: value }
}

fn make_constrained<Env, P>(self, constrainer: fn[Env](T) -> P) -> P {
constrainer(self._value)
}

fn unwrap(self) -> T {
assert_unconstrained();
self._value
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#[builtin(is_unconstrained)]
pub fn is_unconstrained() -> bool {}
#[builtin(assert_unconstrained)]
pub fn assert_unconstrained() {}
struct Unconstrained<T> {
_value: T,
}
impl<T> Unconstrained<T> {
fn new(value: T) -> Self {
Self { _value: value }
}
fn make_constrained<Env, P>(self, constrainer: fn[Env](T) -> P) -> P {
constrainer(self._value)
}
fn unwrap(self) -> T {
assert_unconstrained();
self._value
}
}
#[builtin(is_unconstrained)]
pub fn is_unconstrained() -> bool {}
struct Unconstrained<T> {
_value: T,
}
impl<T> Unconstrained<T> {
fn new(value: T) -> Self {
Self { _value: value }
}
fn make_constrained<Env, P>(self, constrainer: fn[Env](T) -> P) -> P {
constrainer(self._value)
}
fn unwrap(self) -> T {
assert(is_unconstrained(), "User friendly error message");
self._value
}
}

github-merge-queue bot pushed a commit to noir-lang/noir that referenced this pull request Jun 6, 2024
# Description

## Problem\*

Resolves <!-- Link to GitHub Issue -->

## Summary\*

This PR adds a new lint inspired by the review of
AztecProtocol/aztec-packages#6751 which enforces
that oracle functions must be marked as unconstrained.

I've also moved the error for calling an oracle function from a
constrained function into the frontend rather than waiting until ACIR
gen.

## Additional Context



## Documentation\*

Check one:
- [x] 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.
- [x] I have formatted the changes with [Prettier](https://prettier.io/)
and/or `cargo fmt` on default settings.
Thunkar added a commit that referenced this pull request Jul 5, 2024
…nup (#7354)

During the development of the defunct
#6751 I realized we
weren't constraining the args_hash computation in public interfaces, but
promptly forgot during the offsite craze.

This PR fixes that and cleanups the interface computation for
readability. I would have loved to include the checks in the call
interface itself instead of having it in the less clear macros, but we
remove an `if` statement if the function takes no args, which in circuit
land is very good.
AztecBot pushed a commit to AztecProtocol/aztec-nr that referenced this pull request Jul 9, 2024
…nup (#7354)

During the development of the defunct
AztecProtocol/aztec-packages#6751 I realized we
weren't constraining the args_hash computation in public interfaces, but
promptly forgot during the offsite craze.

This PR fixes that and cleanups the interface computation for
readability. I would have loved to include the checks in the call
interface itself instead of having it in the less clear macros, but we
remove an `if` statement if the function takes no args, which in circuit
land is very good.
@nventuro
Copy link
Contributor

nventuro commented Sep 7, 2024

Closing in favor of #8232 etc.

@nventuro nventuro closed this Sep 7, 2024
@nventuro nventuro deleted the gj/wrapped_unconstrained branch September 7, 2024 00:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants