Problem
It's currently not obvious when we're calling an unconstrained function (and introducing unconstrained values into the circuit). It's then quite difficult to rule out that any under-constrained bugs exist as any function call is a potential source.
This would be easier if we could limit the potential surface for this class of bugs such that Noir developers only need to worry about them when dealing with unconstrained functions and the compiler will flag up when this is the case.
Potential Solution
Noir code which makes use of unconstrained functions can be thought of as analogous to unsafe Rust. In Rust, unsafe blocks can be used to temporarily allow breaking some of the compiler's guarantees around memory safety before passing a value back to safe Rust which can be handled similarly to any other safe value (assuming that the code ran in the unsafe block is correct).
If we map this concept over to Noir, we can temporarily break the compiler guarantee that the prover will be constrained to follow the specified logic. Like in unsafe Rust, if we were to write entirely safe code in an unsafe block then it's equivalent to if the block was safe, so an unsafe block in Noir should still lay down constraints except it should also allow introducing unsafe values to the circuit (through unconstrained function calls).
An example of how it can be used
fn is_valid_impl(context: &mut PrivateContext, message_field: Field) -> pub bool {
// Load public key from storage
let storage = Storage::init(Context::private(context));
let public_key = storage.public_key.get_note();
// Load auth witness
let witness: [Field; 64] = get_auth_witness(message_field);
let mut signature: [u8; 64] = [0; 64];
for i in 0..64 {
signature[i] = witness[i] as u8;
}
// Verify payload signature using Ethereum's signing scheme
// Note that noir expects the hash of the message/challenge as input to the ECDSA verification.
let hashed_message: [u8; 32] = std::hash::sha256(message_field.to_be_bytes(32));
let verification = std::ecdsa_secp256k1::verify_signature(public_key.x, public_key.y, signature, hashed_message);
assert(verification == true);
true
}
With the addition of unsafe blocks this could look something like:
fn is_valid_impl(context: &mut PrivateContext, message_field: Field) -> pub bool {
// Load public key from storage
let storage = Storage::init(Context::private(context));
let public_key = storage.public_key.get_note();
// Note that noir expects the hash of the message/challenge as input to the ECDSA verification.
let hashed_message: [u8; 32] = std::hash::sha256(message_field.to_be_bytes(32));
unsafe {
// Safety: Prover must provide an ECDSA signature over `hashed_message` which matches the stored public key.
// Load auth witness
let witness: [Field; 64] = get_auth_witness(message_field);
let mut signature: [u8; 64] = [0; 64];
for i in 0..64 {
signature[i] = witness[i] as u8;
}
// Verify payload signature using Ethereum's signing scheme
let verification = std::ecdsa_secp256k1::verify_signature(public_key.x, public_key.y, signature, hashed_message);
assert(verification == true);
};
true
}
Similarly to in Rust we could add a lint which requires that unsafe blocks must contain a safety comment which explains why the code is sound.
I've got a messy implementation of this in #4429 which adds an unsafe flag to blocks and then during type checking we throw an error if we perform an unconstrained function call inside a constrained function but outside of an unsafe block.
Problem
It's currently not obvious when we're calling an unconstrained function (and introducing unconstrained values into the circuit). It's then quite difficult to rule out that any under-constrained bugs exist as any function call is a potential source.
This would be easier if we could limit the potential surface for this class of bugs such that Noir developers only need to worry about them when dealing with unconstrained functions and the compiler will flag up when this is the case.
Potential Solution
Noir code which makes use of unconstrained functions can be thought of as analogous to unsafe Rust. In Rust,
unsafeblocks can be used to temporarily allow breaking some of the compiler's guarantees around memory safety before passing a value back to safe Rust which can be handled similarly to any other safe value (assuming that the code ran in theunsafeblock is correct).If we map this concept over to Noir, we can temporarily break the compiler guarantee that the prover will be constrained to follow the specified logic. Like in unsafe Rust, if we were to write entirely safe code in an
unsafeblock then it's equivalent to if the block was safe, so anunsafeblock in Noir should still lay down constraints except it should also allow introducing unsafe values to the circuit (through unconstrained function calls).An example of how it can be used
With the addition of
unsafeblocks this could look something like:Similarly to in Rust we could add a lint which requires that
unsafeblocks must contain a safety comment which explains why the code is sound.I've got a messy implementation of this in #4429 which adds an unsafe flag to blocks and then during type checking we throw an error if we perform an unconstrained function call inside a constrained function but outside of an
unsafeblock.