An unconstrained function in Noir is a function that executes outside the ZK constraint system. It produces witness values during proof generation but does not add any constraints to the circuit. The Noir runtime can run arbitrary logic — loops over dynamic bounds, branching, complex calculations — inside an unconstrained function, then use the results in constrained code where they are verified cheaply.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/noir-lang/noir/llms.txt
Use this file to discover all available pages before exploring further.
The unconstrained keyword
Mark a function as unconstrained by placing the unconstrained keyword before fn:
unsafe block:
Why unconstrained functions exist
Zero-knowledge constraint systems can only express computation as arithmetic gates. Some operations that are trivial to compute are expensive to express as constraints:| Operation | As constraints | As Brillig |
|---|---|---|
| Integer division | Many gates (Euclidean algorithm) | One instruction |
| Square root | Many multiplications + range checks | One sqrt call |
| Byte decomposition | Multiple bit-range constraints | Bit shifts + masking |
| Sorting | Comparison network (many gates) | Standard sort |
Witness generation vs. proving
Understanding the two phases of Noir execution clarifies why this is safe:- Witness generation — the prover runs the full Noir program (including unconstrained functions) on their private inputs to assign a concrete value to every wire in the circuit. Unconstrained code runs here.
- Proving — the backend generates a ZK proof that the witness satisfies all the ACIR constraints. Unconstrained code does not appear here.
Brillig: the unconstrained VM
Unconstrained functions compile to Brillig bytecode rather than ACIR opcodes. The Brillig VM executes this bytecode during witness generation. Unlike ACIR, Brillig supports:- Loops over bounds not known at compile time
breakandcontinue- Dynamic memory access
- Arbitrary branching
BrilligCall opcode. The outputs become witness values available to the surrounding ACIR constraints.
Examples
Byte decomposition
The naive constrained approach to converting au72 to [u8; 8] uses bit-shifts and AND operations, which are expensive in a constraint system. Moving the decomposition to an unconstrained function and verifying the result by reconstruction saves significant gate count:
Division hint
Division is cheaper to verify than to compute. Calculate the quotient and remainder in Brillig, then assert the relationship holds:Square root hint
Security checks
The Noir compiler includes two passes that check whether unconstrained call outputs are properly constrained. Independent subgraph detection checks whether any values in the final circuit are disconnected from both the inputs and outputs — a sign of a missing constraint. This check runs by default and can be disabled with--skip-underconstrained-check.
Brillig manual constraint coverage checks that every output of a Brillig call (including every element of array outputs) is involved in a constraint against either an input to that call or a constant. For example:
factored[0] and factored[1] are both involved in the product assertion, so the call is considered properly covered. This check runs by default and can be disabled with --skip-brillig-constraints-check.
When to use unconstrained functions
Use unconstrained functions when:- The computation is hard to express as constraints but easy to verify. Division, square roots, sorting, and byte decompositions all fall into this category.
- You are implementing an oracle. Oracles are always declared as
unconstrainedbecause they fetch data from outside the circuit. See the oracles guide. - You want to branch on runtime values. Constrained code uses all branches; unconstrained code can short-circuit.
- You need loops over runtime-determined bounds. Brillig supports this natively; ACIR requires loop unrolling at compile time.