The Noir Programming Language
This version of the book is being released with the public alpha. There will be a lot of features that are missing in this version, however the syntax and the feel of the language will mostly be completed.
What is Noir?
Noir is a domain specific language for creating and verifying proofs. It's design choices are influenced heavily by Rust.
What's new about Noir?
Noir is much more simple and flexible in design as it does not compile immediately to a fixed NP-complete language. Instead Noir compiles to an intermediate language which itself can be compiled to an arithmetic circuit or a rank-1 constraint system. This in itself brings up a few challenges within the design process, but allows one to decouple the programming language completely from the backend. This is similar in theory to LLVM.
Who is Noir for?
Noir can be used for a variety of purposes.
Ethereum Developers
Noir currently includes a command to publish a contract which verifies your Noir program. This will be modularised in the future, however as of the alpha you can use the contract
command to create it.
Protocol Developers
As a protocol developer, you may not want to use the Aztec backend due to it not being a fit for your stack or maybe you simply want to use a different proving system. Since Noir does not compile to a specific proof system, it is possible for protocol developers to replace the PLONK based proving system with a different proving system altogether.
Blockchain developers
As a blockchain developer, you will be constrained by parameters set by your blockchain, ie the proving system and smart contract language has been pre-defined. In order for you to use Noir in your blockchain, a proving system backend must be implemented for it and a smart contract interface must be implemented for it.
Getting Started
In this section we will discuss, installing Noir and running your first program.
Nargo
nargo
is a command line tool for interacting with Noir programs (e.g. compiling, proving, verifying and more).
Alternatively, the interactions can also be performed in TypeScript.
Installation
There are two approaches to install Nargo:
Optionally you can also install Noir VS Code extension for syntax highlighting.
Option 1: Binaries
Step 1
Paste and run the following in the terminal to extract and install the binary:
macOS / Linux: If you are prompted with
Permission denied
when running commands, prependsudo
and re-run it.
macOS (Apple Silicon)
mkdir -p $HOME/.nargo/bin && \
curl -o $HOME/.nargo/bin/nargo-aarch64-apple-darwin.tar.gz -L https://github.com/noir-lang/noir/releases/download/nightly/nargo-aarch64-apple-darwin.tar.gz && \
tar -xvf $HOME/.nargo/bin/nargo-aarch64-apple-darwin.tar.gz -C $HOME/.nargo/bin/ && \
echo '\nexport PATH=$PATH:$HOME/.nargo/bin' >> ~/.zshrc && \
source ~/.zshrc
macOS (Intel)
mkdir -p $HOME/.nargo/bin && \
curl -o $HOME/.nargo/bin/nargo-x86_64-apple-darwin.tar.gz -L https://github.com/noir-lang/noir/releases/download/nightly/nargo-x86_64-apple-darwin.tar.gz && \
tar -xvf $HOME/.nargo/bin/nargo-x86_64-apple-darwin.tar.gz -C $HOME/.nargo/bin/ && \
echo '\nexport PATH=$PATH:$HOME/.nargo/bin' >> ~/.zshrc && \
source ~/.zshrc
Windows (PowerShell)
Open PowerShell as Administrator and run:
mkdir -f -p "$env:USERPROFILE\.nargo\bin\"; `
Invoke-RestMethod -Method Get -Uri https://github.com/noir-lang/noir/releases/download/nightly/nargo-x86_64-pc-windows-msvc.zip -Outfile "$env:USERPROFILE\.nargo\bin\nargo-x86_64-pc-windows-msvc.zip"; `
Expand-Archive -Path "$env:USERPROFILE\.nargo\bin\nargo-x86_64-pc-windows-msvc.zip" -DestinationPath "$env:USERPROFILE\.nargo\bin\"; `
$Reg = "Registry::HKLM\System\CurrentControlSet\Control\Session Manager\Environment"; `
$OldPath = (Get-ItemProperty -Path "$Reg" -Name PATH).Path; `
$NewPath = $OldPath + ’;’ + "$env:USERPROFILE\.nargo\bin\"; `
Set-ItemProperty -Path "$Reg" -Name PATH –Value "$NewPath"; `
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
Linux (Bash)
See GitHub Releases for additional platform specific binaries.
mkdir -p $HOME/.nargo/bin && \
curl -o $HOME/.nargo/bin/nargo-x86_64-unknown-linux-gnu.tar.gz -L https://github.com/noir-lang/noir/releases/download/nightly/nargo-x86_64-unknown-linux-gnu.tar.gz && \
tar -xvf $HOME/.nargo/bin/nargo-x86_64-unknown-linux-gnu.tar.gz -C $HOME/.nargo/bin/ && \
echo '\nexport PATH=$PATH:$HOME/.nargo/bin' >> ~/.bashrc && \
source ~/.bashrc
Step 2
Check if the installation was successful by running nargo --help
.
macOS: If you are prompted with an OS alert, right-click and open the nargo executable from Finder. Close the new terminal popped up and
nargo
should now be accessible.
For a successful installation, you should see something similar to the following after running the command:
$ nargo --help
Noir's package manager
Usage: nargo <COMMAND>
Commands:
check Checks the constraint system for errors
codegen-verifier Generates a Solidity verifier smart contract for the program
compile Compile the program and its secret execution trace into ACIR format
new Create a new binary project
execute Executes a circuit to calculate its return value
prove Create proof for this program. The proof is returned as a hex encoded string
verify Given a proof and a program, verify whether the proof is valid
test Run the tests for this program
gates Counts the occurrences of different gates in circuit
help Print this message or the help of the given subcommand(s)
Option 2: Compile from Source
Setup
-
Download Noir's source code from Github by running:
git clone [email protected]:noir-lang/noir.git
-
Change directory into the Noir project by running:
cd noir
There are then two approaches to proceed, differing in how the proving backend is installed:
Option 2.1: Install Executable with WASM backend
-
Install Nargo by running:
cargo install --locked --path=crates/nargo --no-default-features --features plonk_bn254_wasm
Option 2.2: Install Executable with Native Backend
The barretenberg proving backend is written in C++, hence compiling it from source would first require certain dependencies to be installed.
-
Install CMake, LLVM and OpenMP:
macOS
Installing through Homebrew is recommended:
brew install cmake llvm libomp
Ubuntu (Linux)
sudo apt update && sudo apt install clang lld cmake libomp-dev
Other variants of Linux will need to adjust the commands for their package manager.
Windows
TBC
-
Install Nargo by running:
cargo install --locked --path=crates/nargo
Verify Installation
-
Check if the installation was successful by running
nargo --help
:$ nargo --help Noir's package manager Usage: nargo <COMMAND> Commands: check Checks the constraint system for errors codegen-verifier Generates a Solidity verifier smart contract for the program compile Compile the program and its secret execution trace into ACIR format new Create a new binary project execute Executes a circuit to calculate its return value prove Create proof for this program. The proof is returned as a hex encoded string verify Given a proof and a program, verify whether the proof is valid test Run the tests for this program gates Counts the occurrences of different gates in circuit help Print this message or the help of the given subcommand(s)
Commands
nargo help [subcommand]
Prints the list of available commands or specific information of a subcommand.
Arguments
<subcommand>
- The subcommand whose help message to display
nargo new <package_name> [path]
Creates a new Noir project.
Arguments
<package_name>
- Name of the package[path]
- The path to save the new project
nargo check
Generate the Prover.toml
and Verifier.toml
files for specifying prover and verifier in/output values of the Noir program respectively.
nargo execute
Runs the Noir program and prints its return value.
<witness_name>
- The name of the witness
nargo prove <proof_name>
Creates a proof for the program.
Arguments
<proof_name>
- The name of the proof
nargo verify <proof>
Given a proof and a program, verify whether the proof is valid.
Arguments
<proof>
- The proof to verify
nargo codegen-verifier
Generate a Solidity verifier smart contract for the program.
nargo preprocess <build_artifact>
Generate proving and verification keys from a build artifact file.
nargo compile <circuit_name>
Compile the program and its secret execution trace into a JSON build artifact file containing the ACIR representation, and the ABI of the circuit. This build artifact can then be used to generate and verify proofs.
Arguments
<circuit_name>
- The name of the circuit file
The files compiled can be passed into a TypeScript project for proving and verification. See the TypeScript section to learn more.
Hello, World
Now that we have installed Nargo, it is time to make our first hello world program!
Create a Project Directory
Noir code can live anywhere on your computer. Let us create a projects folder in the home directory to house our Noir programs.
For Linux, macOS, and Windows PowerShell, create the directory and change directory into it by running:
$ mkdir ~/projects
$ cd ~/projects
For Windwows CMD, run:
> mkdir "%USERPROFILE%\projects"
> cd /d "%USERPROFILE%\projects"
Create Our First Nargo Project
Now that we are in the projects directory, create a new Nargo project by running:
$ nargo new hello_world
Note:
hello_world
can be any arbitrary project name, we are simply usinghello_world
for demonstration.In production, the common practice is to name the project folder as
circuits
for better identifiability when sitting alongside other folders in the codebase (e.g.contracts
,scripts
,test
).
A hello_world
folder would be created. Similar to Rust, the folder houses src/main.nr and Nargo.toml that contains the source code and environmental options of your Noir program respectively.
Intro to Noir Syntax
Let us take a closer look at main.nr. The default main.nr generated should look like this:
fn main(x : Field, y : pub Field) { constrain x != y; }
The first line of the program specifies the program's inputs:
#![allow(unused)] fn main() { x : Field, y : pub Field }
Program inputs in Noir are private by default (e.g. x
), but can be labeled public using the keyword pub
(e.g. y
). To learn more about private and public values, check the Data Types section.
The next line of the program specifies its body:
#![allow(unused)] fn main() { constrain x != y; }
The Noir syntax constrain
can be interpreted as something similar to assert
in other languages.
For more Noir syntax, check the Language Concepts chapter.
Build In/Output Files
Change directory into hello_world and build in/output files for your Noir program by running:
$ cd hello_world
$ nargo check
Two additional files would be generated in your project directory:
Prover.toml houses input values, and Verifier.toml houses public values.
Prove Our Noir Program
Now that the project is set up, we can create a proof of correct execution on our Noir program.
Fill in input values for execution in the Prover.toml file. For example:
x = "1"
y = "2"
Prove the valid execution of your Noir program with your preferred proof name, for example p
:
$ nargo prove p
A new folder proofs would then be generated in your project directory, containing the proof file p.proof
.
The Verifier.toml file would also be updated with the public values computed from program execution (in this case the value of y
):
y = "0x0000000000000000000000000000000000000000000000000000000000000002"
Note: Values in Verifier.toml are computed as 32-byte hex values.
Verify Our Noir Program
Once a proof is generated, we can verify correct execution of our Noir program by verifying the proof file.
Verify your proof of name p
by running:
$ nargo verify p
The verification will complete in silence if it is successful. If it fails, it will log the corresponding error instead.
Congratulations, you have now created and verified a proof for your very first Noir program!
In the next section, we will go into more detail on each step performed.
Breakdown
This section breaks down our hello world program in section 1.2.
We elaborate on the project structure and what the prove
and verify
commands did in the previous section.
Anatomy of a Nargo Project
Upon creating a new project with nargo new
and building the in/output files with nargo check
commands, you would get a minimal Nargo project of the following structure:
- src
- Prover.toml
- Verifier.toml
- Nargo.toml
The source directory src holds the source code for your Noir program. By default only a main.nr file will be generated within it.
Prover.toml is used for specifying the input values for executing and proving the program. Optionally you may specify expected output values for prove-time checking as well.
Verifier.toml contains public in/output values computed when executing the Noir program.
Nargo.toml contains the environmental options of your project.
proofs and contract directories will not be immediately visible until you create a proof or verifier contract respectively.
main.nr
The main.nr file contains a main
method, this method is the entry point into your Noir program.
In our sample program, main.nr looks like this:
fn main(x : Field, y : Field) { constrain x != y; }
The parameters x
and y
can be seen as the API for the program and must be supplied by the prover. Since neither x
nor y
is marked as public, the verifier does not supply any inputs, when verifying the proof.
The prover supplies the values for x
and y
in the Prover.toml file.
As for the program body, constrain
ensures the satisfaction of the condition (e.g. x != y
) is constrained by the proof of the execution of said program (i.e. if the condition was not met, the verifier would reject the proof as an invalid proof).
Prover.toml
The Prover.toml file is a file which the prover uses to supply his witness values(both private and public).
In our hello world program the Prover.toml file looks like this:
x = "1"
y = "2"
When the command nargo prove my_proof
is executed, two processes happen:
-
Noir creates a proof that
x
which holds the value of1
andy
which holds the value of2
is not equal. This not equal constraint is due to the lineconstrain x != y
. -
Noir creates and stores the proof of this statement in the proofs directory and names the proof file my_proof. Opening this file will display the proof in hex format.
Verifying a Proof
When the command nargo verify my_proof
is executed, two processes happen:
-
Noir checks in the proofs directory for a file called my_proof
-
If that file is found, the proof's validity is checked
Note: The validity of the proof is linked to the current Noir program; if the program is changed and the verifier verifies the proof, it will fail because the proof is not valid for the modified Noir program.
In production, the prover and the verifier are usually two separate entities. A prover would retrieve the necessary inputs, execute the Noir program, generate a proof and pass it to the verifier. The verifier would then retrieve the public inputs from usually external sources and verifies the validity of the proof against it.
Take a private asset transfer as an example:
A user on browser as the prover would retrieve private inputs (e.g. the user's private key) and public inputs (e.g. the user's encrypted balance on-chain), compute the transfer, generate a proof and submit it to the verifier smart contract.
The verifier contract would then draw the user's encrypted balance directly from the blockchain and verify the proof submitted against it. If the verification passes, additional functions in the verifier contract could trigger (e.g. approve the asset transfer).
Solidity Verifier
For certain applications, it may be desirable to run the verifier as a smart contract instead of on a local machine.
Compile a Solidity verifier contract for your Noir program by running:
nargo codegen-verifier
A new contract
folder would then be generated in your project directory, containing the Solidity file plonk_vk.sol
. It can be deployed on any EVM blockchain acting as a verifier smart contract.
Note: It is possible to compile verifier contracts of Noir programs for other smart contract platforms as long as the proving backend supplies an implementation.
Barretenberg, the default proving backend Nargo is integrated with, supports compilation of verifier contracts in Solidity only for the time being.
TypeScript
Interactions with Noir programs can also be performed in TypeScript, which can come in handy when writing tests or when working in TypeScript-based projects like Hardhat.
The following sections use the Standard Noir Example as an example to dissect a typical Noir workflow in TypeScript, with specific focus on its:
- Test script
1_mul.ts
- Noir program
main.nr
- Verifier contract generator script
generate_sol_verifier.ts
You are also welcome to revisit the full scripts 1_mul.ts
of Standard Noir Example and mm.ts
of Mastermind in Noir anytime for inspirations.
Setup
Install Noir dependencies in your project by running:
$ yarn add @noir-lang/noir_wasm @noir-lang/barretenberg @noir-lang/aztec_backend
And import the applicable functions into your TypeScript file by adding:
// 1_mul.ts
import { compile, acir_read_bytes } from '@noir-lang/noir_wasm';
import { setup_generic_prover_and_verifier, create_proof, verify_proof, create_proof_with_witness } from '@noir-lang/barretenberg/dest/client_proofs';
import { packed_witness_to_witness, serialise_public_inputs, compute_witnesses } from '@noir-lang/aztec_backend';
Compiling
To begin proving and verifying a Noir program, it first needs to be compiled by calling noir_wasm
's compile
function:
// 1_mul.ts
const compiled_program = compile(path.resolve(__dirname, "../circuits/src/main.nr"));
The compiled_program
returned by the function contains the ACIR and the Application Binary Interface (ABI) of your Noir program. They shall be stored for proving your program later:
// 1_mul.ts
let acir = compiled_program.circuit;
const abi = compiled_program.abi;
Note: Compiling with
noir_wasm
may lack some of the newer features thatnargo compile
offers. See the Proving and Verifying Externally Compiled Files section to learn more.
Specifying Inputs
Having obtained the compiled program, the program inputs shall then be specified in its ABI.
Standard Noir Example is a program that multiplies input x
with input y
and returns the result:
# main.nr
fn main(x: u32, y: pub u32) -> pub u32 {
let z = x * y;
z
}
Hence, one valid scenario for proving could be x = 3
, y = 4
and return = 12
:
// 1_mul.ts
abi.x = 3;
abi.y = 4;
abi.return = 12;
Info: Return values are also required to be specified, as they are merely syntax sugar of inputs with equality constraints.
Tip: To best protect the private inputs in your program (if applicable) from public knowledge, you should consider minimizing any passing around of inputs and deleting the inputs on the prover instance once the proof is created when designing your program.
Initializing Prover & Verifier
Prior to proving and verifying, the prover and verifier have to first be initialized by calling barretenberg
's setup_generic_prover_and_verifier
with your Noir program's ACIR:
// 1_mul.ts
let [prover, verifier] = await setup_generic_prover_and_verifier(acir);
Proving
The Noir program can then be executed and proved by calling barretenberg
's create_proof
function:
// 1_mul.ts
const proof = await create_proof(prover, acir, abi);
Verifying
The proof
obtained can be verified by calling barretenberg
's verify_proof
function:
// 1_mul.ts
const verified = await verify_proof(verifier, proof);
The function should return true
if the entire process is working as intended, which can be asserted if you are writing a test script:
// 1_mul.ts
expect(verified).eq(true);
Verifying with Smart Contract
Alternatively, a verifier smart contract can be generated and used for verifying Noir proofs in TypeScript as well.
This could be useful if the Noir program is designed to be decentrally verified and/or make use of decentralized states and logics that is handled at the smart contract level.
To generate the verifier smart contract:
// generate_sol_verifier.ts
// Imports
import { writeFileSync } from 'fs';
...
// Generate verifier contract
const sc = verifier.SmartContract();
syncWriteFile("../contracts/plonk_vk.sol", sc);
...
function syncWriteFile(filename: string, data: any) {
writeFileSync(join(__dirname, filename), data, {
flag: 'w',
});
}
To verify a Noir proof using the verifier contract:
// 1_mul.ts
// Imports
import { ethers } from "hardhat";
import { Contract, ContractFactory, utils } from 'ethers';
...
// Deploy verifier contract
let Verifier: ContractFactory;
let verifierContract: Contract;
before(async () => {
Verifier = await ethers.getContractFactory("TurboVerifier");
verifierContract = await Verifier.deploy();
});
...
// Verify proof
const sc_verified = await verifierContract.verify(proof);
expect(sc_verified).eq(true)
Proving and Verifying Externally Compiled Files
In some cases, noir_wasm
may lack some of the newer features for compiling Noir programs due to separated upgrade workflows.
To benefit from the best of both worlds, a Noir program can be compiled with nargo compile
, with the .acir
and .tr
files then passed into your TypeScript project for proving and verifying:
// 1_mul.ts
// Parse acir
let acirByteArray = path_to_uint8array(path.resolve(__dirname, '../circuits/build/p.acir'));
let acir = acir_read_bytes(acirByteArray);
// Parse witness
let witnessByteArray = path_to_uint8array(path.resolve(__dirname, '../circuits/build/p.tr'));
const barretenberg_witness_arr = await packed_witness_to_witness(acir, witnessByteArray);
...
// Create proof
const proof = await create_proof_with_witness(prover, barretenberg_witness_arr);
Info: The
.acir
file is the ACIR of your Noir program, and the.tr
file is the witness file. The witness file can be considered as program inputs parsed for your program's ACIR.
See the Commands section to learn more about the nargo compile
command.
Merkle Proof
Let's walk through an example of a merkle membership proof in Noir that proves that a given leaf is in a merkle tree.
use dep::std;
fn main(message : [Field; 62], index : Field, hashpath : [Field; 40], root : Field) {
let leaf = std::hash::hash_to_field(message);
let is_member = std::merkle::check_membership(root, leaf, index, hashpath);
constrain is_member == 1;
}
The above code uses the noir standard library to call both of the aforementioned components.
let leaf = std::hash::hash_to_field(message);
The message is hashed using hash_to_field
. The specific hash function that is being used is chosen by the backend. The only requirement is that this hash function can heuristically be used as a random oracle. If only collision resistance is needed, then one can call std::hash::pedersen
instead.
let is_member = std::merkle::check_membership(root, leaf, index, hashpath);
The leaf is then passed to a check_membership proof with the root, index and hashpath. is_member
returns 1 if the leaf is a member of the merkle tree with the specified root, at the given index.
Note: It is possible to re-implement the merkle tree implementation without standard library. However, for most usecases, it is enough. In general, the standard library will always opt to be as conservative as possible, while striking a balance between efficiency.
An example, the merkle membership proof, only requires a hash function that has collision resistance, hence a hash function like Pedersen is allowed, which in most cases is more efficient than the even more conservative sha256.
constrain is_member == 1;
This last line, constrains the variable to be equal to 1. If 1 was changed to 0, this would create a proof that the leaf was not present at that specific index for that specific root. Importantly, it would not prove that this leaf was not in the merkle tree.
Example Project: https://github.com/vezenovm/simple_shield
Language Concepts
In this chapter, we will go over the concepts that are being used in Noir. Specifically, we will learn about types, control flow, comments, functions and declarations. For some concepts, we also explain the rationale as to why they were designed in this particular way. Recurring themes you will encounter in this section are simplicity and safety.
Mutability
Variables in noir can be declared mutable via the mut
keyword.
Mutable variables can be reassigned to via
an assignment expression.
let x = 2;
x = 3; // error: x must be mutable to be assigned to
let mut y = 3;
let y = 4; // OK
The mut
modifier can also apply to patterns:
let (a, mut b) = (1, 2);
a = 11; // error: a must be mutable to be assigned to
b = 12; // OK
let mut (c, d) = (3, 4);
c = 13; // OK
d = 14; // OK
// etc.
let MyStruct { x: mut y } = MyStruct { x: a }
// y is now in scope
Note that mutability in noir is local and everything is passed by value, so if a called function mutates its parameters then the parent function will keep the old value of the parameters.
fn main() -> Field {
let x = 3;
helper(x);
x // x is still 3
}
fn helper(mut x: i32) {
x = 4;
}
Constants
A constant type is a value that does not change per circuit instance. This is different to a witness which changes per proof. If a constant type that is being used in your program is changed, then your circuit will also change.
Below we show how to declare a constant value:
fn main() {
let a: comptime Field = 5;
// `comptime Field` can also be inferred:
let a = 5;
}
Note that variables declared as mutable may not be constants:
fn main() {
// error: Cannot mark a comptime type as mutable - any mutation would remove its const-ness
let mut a: comptime Field = 5;
// a inferred as a private Field here
let mut a = 5;
}
Globals
Noir also supports global variables. However, they must be compile-time variables. If comptime
is not explicitly written in the type annotation the compiler will implicitly specify the declaration as compile-time. They can then be used like any other compile-time variable inside functions. The global type can also be inferred by the compiler entirely. Globals can also be used to specify array annotations for function parameters and can be imported from submodules.
Globals are currently limited to Field, integer, and bool literals.
global N: Field = 5; // Same as `global N: comptime Field = 5`
fn main(x : Field, y : [Field; N]) {
let res = x * N;
constrain res == y[0];
let res2 = x * mysubmodule::N;
constrain res != res2;
}
mod mysubmodule {
use dep::std;
global N: Field = 10;
fn my_helper() -> comptime Field {
let x = N;
x
}
}
Why only local mutability?
Witnesses in a proving system are immutable in nature. Noir aims to closely mirror this setting without applying additional overhead to the user. Modeling a mutable reference is not as straightforward as on conventional architectures and would incur some possibly unexpected overhead.
Data Types
Every value in Noir has a type, which determines which operations are valid for it.
All values in Noir are fundamentally composed of Field
elements. For a more approachable developing experience, abstractions are added on top to introduce different data types in Noir.
Noir has two category of data types: primitive types (e.g. Field
, integers, bool
) and compound types that group primitive types (e.g. arrays, tuples, structs). Each value can either be private or public.
Private & Public Types
A private value is known only to the Prover, while a public value is known by both the Prover and Verifier. All primitive types (including individual fields of compound types) in Noir are private by default, and can be marked public when certain values are intended to be revealed to the Verifier.
Note: For public values defined in Noir programs paired with smart contract verifiers, once the proofs are verified on-chain the values can be considered known to everyone that has access to that blockchain.
Public data types are treated no differently to private types apart from the fact that their values will be revealed in proofs generated. Simply changing the value of a public type will not change the circuit (where the same goes for changing values of private types as well).
Private values are also referred to as witnesses sometimes.
Note: The terms private and public when applied to a type (e.g.
pub Field
) have a different meaning than when applied to a function (e.g.pub fn foo() {}
).The former is a visibility modifier for the Prover to interpret if a value should be made known to the Verifier, while the latter is a visibility modifier for the compiler to interpret if a function should be made accessible to external Noir programs like in other languages.
pub Modifier
All data types in Noir are private by default. Types are explicitly declared as public using the pub
modifier:
fn main(x : Field, y : pub Field) -> pub Field {
x + y
}
In this example, x
is private while y
and x + y
(the return value) are public. Note that visibility is handled per variable, so it is perfectly valid to have one input that is private and another that is public.
Note: Public types can only be declared through parameters on
main
.
Primitive Types
A primitive type represents a single value. They can be private or public.
The Field Type
The field type corresponds to the native field type of the proving backend.
The size of a Noir field depends on the elliptic curve's finite field for the proving backend adopted. For example, a field would be a 254-bit integer when paired with the default TurboPlonk backend that spans the Grumpkin curve.
Fields support integer arithmetic and are often used as the default numeric type in Noir:
fn main(x : Field, y : Field) {
let z = x + y;
}
x
, y
and z
are all private fields in this example. Using the let
keyword we defined a new private value z
constrained to be equal to x + y
.
If proving efficiency is of priority, fields should be used as a default for solving problems. Smaller integer types (e.g. u64
) incur extra range constraints.
Integer Types
An integer type is a range constrained field type. The Noir frontend currently supports unsigned, arbitrary-sized integer types.
An integer type is specified first with the letter u
, indicating its unsigned nature, followed by its length in bits (e.g. 32
). For example, a u32
variable can store a value in the range of \([0,2^{32}-1]\):
fn main(x : Field, y : u32) {
let z = x as u32 + y;
}
x
, y
and z
are all private values in this example. However, x
is a field while y
and z
are unsigned 32-bit integers. If y
or z
exceeds the range \([0,2^{32}-1]\), proofs created will be rejected by the verifier.
Note: The default TurboPlonk backend supports both even (e.g.
u16
,u48
) and odd (e.g.u5
,u3
) sized integer types.
The Boolean Type
The bool
type in Noir has two possible values: true
and false
:
fn main() {
let t = true;
let f: bool = false;
}
Note: When returning a boolean value, it will show up as a value of 1 for
true
and 0 forfalse
in Verifier.toml.
The boolean type is most commonly used in conditionals like if
expressions and constrain
statements. More about conditionals is covered in the Control Flow and Constrain Statement sections.
The String Type
Strings in Noir are fairly basic with their main use being for debugging via std::println.. Since circuit inputs need to be known at compile time, the string length for an input must be hardcoded into the circuit, like so:
// **input** // field = "hello" fn main(string: str<5>) { let hello = "hello"; constrain string == hello; }
String manipulation isn't available at this time, but as long as you make the variable mut
, you can replace it.
Compound Types
A compound type groups together multiple values into one type. Elements within a compound type can be private or public.
The Array Type
An array is one way of grouping together values into one compound type. Array types can be inferred or explicitly specified via the syntax [<Type>; <Size>]
:
fn main(x : Field, y : Field) {
let my_arr = [x, y];
let your_arr: [Field; 2] = [x, y];
}
Here, both my_arr
and your_arr
are instantiated as an array containing two Field
elements.
Array elements can be accessed using indexing:
fn main() {
let a = [1, 2, 3, 4, 5];
let first = a[0];
let second = a[1];
}
All elements in an array must be of the same type (i.e. homogeneous). That is, an array cannot group a Field
value and a u8
value together for example.
The Tuple Type
A tuple collects multiple values like an array, but with the added ability to collect values of different types:
fn main() {
let tup: (u8, u64, Field) = (255, 500, 1000);
}
One way to access tuple elements is via destructuring using pattern matching:
fn main() {
let tup = (1, 2);
let (one, two) = tup;
let three = one + two;
}
Another way to access tuple elements is via direct member access, using a period (.
) followed by the index of the element we want to access. Index 0
corresponds to the first tuple element, 1
to the second and so on:
fn main() {
let tup = (5, 6, 7, 8);
let five = tup.0;
let eight = tup.3;
}
Structs
A struct also allows for grouping multiple values of different types. Unlike tuples, we can also name each field.
Note: The usage of field here refers to each element of the struct and is unrelated to the field type of Noir.
Defining a struct requires giving it a name and listing each field within as <Key>: <Type>
pairs:
struct Animal {
hands: Field,
legs: Field,
eyes: u8,
}
An instance of a struct can then be created with actual values in <Key>: <Value>
pairs in any order. Struct fields are accessible using their given names:
fn main() {
let legs = 4;
let dog = Animal {
eyes: 2,
hands: 0,
legs,
};
let zero = dog.hands;
}
Structs can also be destructured in a pattern, binding each field to a new variable:
fn main() {
let Animal { hands, legs: feet, eyes } = get_octopus();
let ten = hands + feet + eyes as u8;
}
fn get_octopus() -> Animal {
let octopus = Animal {
hands: 0,
legs: 8,
eyes: 2,
};
octopus
}
The new variables can be bound with names different from the original struct field names, as showcased in the legs --> feet
binding in the example above.
Functions
Functions in Noir follow the same semantics of Rust, though Noir does not support early returns.
To declare a function the fn
keyword is used.
fn foo() {}
All parameters in a function must have a type and all types are known at compile time. The parameter is pre-pended with a colon and the parameter type. Multiple parameters are separated using a comma.
fn foo(x : Field, y : pub Field){}
The return type of a function can be stated by using the ->
arrow notation. The function below states that the foo function must return a Field
. If the function returns no value, then the arrow is omitted.
fn foo(x : Field, y : pub Field) -> Field {
x + y
}
Note that a return
keyword is unneeded in this case - the last expression in a function's body is returned.
Call Expressions
Calling a function in Noir is executed by using the function name and passing in the necessary arguments.
Below we show how to call the foo
function from the main
function using a call expression:
fn main(x : Field, y : Field) {
let z = foo(x);
}
fn foo(x : Field) -> Field {
x + x
}
Methods
You can define methods in Noir on any struct type in scope.
struct MyStruct {
foo: Field,
bar: Field,
}
impl MyStruct {
fn new(foo: Field) -> MyStruct {
MyStruct {
foo,
bar: 2,
}
}
fn sum(self) -> Field {
self.foo + self.bar
}
}
fn main() {
let s = MyStruct::new(40);
constrain s.sum() == 42;
}
Methods are just syntactic sugar for functions, so if we wanted to we could also call sum
as follows:
constrain MyStruct::sum(s) == 42
Comments
A comment is a line in your codebase which the compiler ignores, however it can be read by programmers.
Here is a single line comment:
// This is a comment and is ignored
//
is used to tell the compiler to ignore the rest of the line.
Noir doesn't have multi-line comments, but you can emulate them via using //
on each line
// This is a multi line
// comment, that is ignored by
// the compiler
Control Flow
Loops
Noir has one kind of loop: the for
loop. for
loops allow you to repeat a block of code multiple times.
The following block of code between the braces is run 10 times.
for i in 0..10 {
// do something
};
If Expressions
Noir supports if-else
statements. The syntax is most similar to Rust's where it is not required for the statement's conditonal to be surrounded by parentheses.
let a = 0;
let mut x: u32 = 0;
if a == 0 {
if a != 0 {
x = 6;
} else {
x = 2;
}
} else {
x = 5;
constrain x == 5;
}
constrain x == 2;
Operations
Table of Supported Operations
Operation | Description | Requirements |
---|---|---|
+ | Adds two concealed types together | Types must be concealed |
- | Subtracts two concealed types together | Types must be concealed |
* | Multiplies two concealed types together | Types must be concealed |
/ | Divides two concealed types together | Types must be concealed |
^ | XOR two concealed types together | Types must be integer |
& | AND two concealed types together | Types must be integer |
<< | Left shift an integer by another integer amount | Types must be integer |
>> | Right shift an integer by another integer amount | Types must be integer |
! | Bitwise not of a value | Type must be integer or boolean |
< | returns a bool if one value is less than the other | Upper bound must have a known bit size |
<= | returns a bool if one value is less than or equal to the other | Upper bound must have a known bit size |
> | returns a bool if one value is more than the other | Upper bound must have a known bit size |
>= | returns a bool if one value is more than or equal to the other | Upper bound must have a known bit size |
== | returns a bool if one value is equal to the other | Both types must not be constants |
!= | returns a bool if one value is not equal to the other | Both types must not be constants |
Predicate Operators
<,<=, !=, == , >, >=
are known as predicate/comparison operations because they compare two values. This differs from the operations such as +
where the operands are used in computation.
Bitwise Operations Example
fn main(x : Field) {
let y = x as u32;
let z = y & y;
}
z
is implicitly constrained to be the result of y & y
. The &
operand is used to denote bitwise &
.
x & x
would not compile asx
is aField
and not an integer type.
Logical Operators
Noir has no support for the logical operators ||
and &&
.
This is because encoding the short-circuiting that these operators require can be inefficient for Noir's backend.
Instead you can use the bitwise operators |
and &
which operate indentically for booleans, just without the short-circuiting.
let my_val = 5;
let mut flag = 1;
if (my_val > 6) | (my_val == 0) {
flag = 0;
}
constrain flag == 1;
if (my_val != 10) & (my_val < 50) {
flag = 0;
}
constrain flag == 0;
Constrain Statement
Noir includes a special keyword constrain
which will explicitly constrain the predicate/comparison expression that follows to be true.
If this expression is false at runtime, the program will fail to be proven.
Constrain statement example
fn main(x : Field, y : Field) {
constrain x == y;
}
The above snippet compiles because ==
is a predicate operation. Conversely, the following will not compile:
fn main(x : Field, y : Field) {
constrain x + y;
}
The rationale behind this not compiling is due to ambiguity. It is not clear if the above should equate to
x + y == 0
or if it should check the truthiness of the result.
Noir Standard Library
Noir features a standard library with some ready-to-use, built-in structures. To use them, you should import the std
library, like so:
#![allow(unused)] fn main() { use dep::std; }
You should then have these constructs available. For example, you can now call the std::hash::pedersen
function like so:
#![allow(unused)] fn main() { let data : [Field; 2] = [42, 42]; std::hash::pedersen(data); }
Logging
The standard library provides a familiar println
statement you can use. Despite being a limited implementation of rust's println!
macro, this construct can be useful for debugging.
use dep::std; fn main(string: pub str<5>) { let x = 5; std::println(x) }
Field
After declaring a Field, you can use these common methods on it 1:
to_le_bits
Transforms the field into an array of bits, Little Endian.
#![allow(unused)] fn main() { fn to_le_bits<N>(_x : Field, _bit_size: u32) -> [u1; N] }
example:
fn main() { let field = 2 let bits = field.to_le_bits(32); }
to_le_bytes
Transforms into an array of bytes, Little Endian
#![allow(unused)] fn main() { fn to_le_bytes(_x : Field, byte_size: u32) -> [u8] }
example:
fn main() { let field = 2 let bytes = field.to_le_bytes(4); }
to_le_radix
Decomposes into a vector over the specificed base, Little Endian
#![allow(unused)] fn main() { fn to_le_radix(_x : Field, _radix: u32, _result_len: u32) -> [u8] }
example:
fn main() { let field = 2 let radix = field.to_le_radix(256, 4); }
to_be_radix
Decomposes into a vector over the specificed base, Big Endian
#![allow(unused)] fn main() { fn to_be_radix(_x : Field, _radix: u32, _result_len: u32) -> [u8] }
example:
fn main() { let field = 2 let radix = field.to_be_radix(256, 4); }
pow_32
Returns the value to the power of the specificied exponent
#![allow(unused)] fn main() { fn pow_32(self, exponent: Field) -> Field }
example:
fn main() { let field = 2 let pow = field.pow_32(4); constrain pow == 16; }
Array
For convenience, the STD provides some ready-to-use, common methods for arrays1:
len
Returns the length of an array
#![allow(unused)] fn main() { fn len<T, N>(_array: [T; N]) -> comptime Field }
example
fn main() { let array = [42, 42] constrain arr.len() == 2; }
sort
Returns a new sorted array. The original array remains untouched. Notice that this function will only work for arrays of fields or integers, not for any arbitrary type. This is because the sorting logic it uses internally is optimized specifically for these values. If you need a sort function to sort any type, you should use the function sort_via
described below.
#![allow(unused)] fn main() { fn sort<T, N>(_array: [T; N]) -> [T; N] }
example
fn main() { let arr = [42, 32] let sorted = arr.sort(); constrain sorted == [32, 42]; }
sort_via
Sorts the array with a custom comparison function
#![allow(unused)] fn main() { fn sort_via<T, N>(mut a: [T; N], ordering: fn(T, T) -> bool) -> [T; N] }
example
fn main() { let arr = [42, 32] let sorted_ascending = arr.sort_via(|a, b| a < b); constrain sorted_ascending == [32, 42]; // verifies let sorted_descending = arr.sort_via(|a, b| a > b); constrain sorted_descending == [32, 42]; // does not verify }
fold
Applies a function to each element of the array, returning the final accumulated value. The first parameter is the initial value.
#![allow(unused)] fn main() { fn fold<U>(mut accumulator: U, f: fn(U, T) -> U) -> U }
This is a left fold, so the given function will be applied to the accumulator and first element of the array, then the second, and so on. For a given call the expected result would be equivalent to:
#![allow(unused)] fn main() { let a1 = [1]; let a2 = [1, 2]; let a3 = [1, 2, 3]; let f = |a, b| a - b; a1.fold(10, f) //=> f(10, 1) a2.fold(10, f) //=> f(f(10, 1), 2) a3.fold(10, f) //=> f(f(f(10, 1), 2), 3) }
example:
fn main() { let arr = [2,2,2,2,2] let folded = arr.fold(0, |a, b| a + b); constrain folded == 10; }
reduce
Same as fold, but uses the first element as starting element.
#![allow(unused)] fn main() { fn reduce<T, N>(f: fn(T, T) -> T) -> T }
example:
fn main() { let arr = [2,2,2,2,2] let reduced = arr.reduce(|a, b| a + b); constrain reduced == 10; }
all
Returns true if all the elements satisfy the given predicate
#![allow(unused)] fn main() { fn all<T, N>(predicate: fn(T) -> bool) -> bool }
example:
fn main() { let arr = [2,2,2,2,2] let all = arr.all(|a| a == 2); constrain all; }
any
Returns true if any of the elements satisfy the given predicate
#![allow(unused)] fn main() { fn any<T, N>(predicate: fn(T) -> bool) -> bool }
example:
fn main() { let arr = [2,2,2,2,5] let any = arr.any(|a| a == 5); constrain any; }
Migration Note: These methods were previously free functions, called via std::array::len()
. For the sake of ease of use and readability, these functions are now methods and the old syntax for them is now deprecated.
Merkle Trees
check_membership
Returns 1 if the specified leaf is at the given index on a tree
#![allow(unused)] fn main() { fn check_membership(_root : Field, _leaf : Field, _index : Field, _hash_path: [Field]) -> Field }
example:
/** * index = "0" priv_key = "0x000000000000000000000000000000000000000000000000000000616c696365" secret = "0x1929ea3ab8d9106a899386883d9428f8256cfedb3c4f6b66bf4aa4d28a79988f" root = "0x2f36d4404719a30512af45be47c9732e916cb131933102b04ba6432602db209c" hash_path = [ "0x1e61bdae0f027b1b2159e1f9d3f8d00fa668a952dddd822fda80dc745d6f65cc", "0x0e4223f3925f98934393c74975142bd73079ab0621f4ee133cee050a3c194f1a", "0x2fd7bb412155bf8693a3bd2a3e7581a679c95c68a052f835dddca85fa1569a40" ] */ fn main(root : Field, index : Field, hash_path : [Field; 3], secret: Field, priv_key: Field) { constrain index == index; let pubkey = std::scalar_mul::fixed_base(priv_key); let pubkey_x = pubkey[0]; let pubkey_y = pubkey[1]; let note_commitment = std::hash::pedersen([pubkey_x, pubkey_y, secret]); let root = std::merkle::check_membership(root, note_commitment[0], index, hash_path); std::println(root); }
check_membership_in_noir
Behaves exactly the same as above, but it's computed in Noir in order to accept many backends.
#![allow(unused)] fn main() { fn check_membership_in_noir(root : Field, leaf : Field, index : Field, hash_path: [Field]) -> Field }
For examples, you can literally replace check_membership
for this method, in the above example.
compute_root_from_leaf
Returns the root of the tree from the provided leaf and its hashpath, using a pedersen hash
#![allow(unused)] fn main() { fn compute_root_from_leaf(leaf : Field, index : Field, hash_path: [Field]) -> Field }
example:
/** index = "0" priv_key = "0x000000000000000000000000000000000000000000000000000000616c696365" secret = "0x1929ea3ab8d9106a899386883d9428f8256cfedb3c4f6b66bf4aa4d28a79988f" note_hash_path = [ "0x1e61bdae0f027b1b2159e1f9d3f8d00fa668a952dddd822fda80dc745d6f65cc", "0x0e4223f3925f98934393c74975142bd73079ab0621f4ee133cee050a3c194f1a", "0x2fd7bb412155bf8693a3bd2a3e7581a679c95c68a052f835dddca85fa1569a40" ] */ fn main(index : Field, priv_key : Field, secret : Field, note_hash_path : [Field; 3]) { constrain index == index; let pubkey = std::scalar_mul::fixed_base(priv_key); let pubkey_x = pubkey[0]; let pubkey_y = pubkey[1]; let note_commitment = std::hash::pedersen([pubkey_x, pubkey_y, secret]); let root = std::merkle::compute_root_from_leaf(note_commitment[0], index, note_hash_path); std::println(root); }
Cryptographic primitives
Some cryptographic primitives are already developed and ready-to-use for any Noir project:
sha256
Given an array of bytes, returns the sha256 of it:
fn main() { let x = [163, 117, 178, 149] // some random bytes let hash = std::hash::sha256(x); }
blake2s
Given an array of bytes, returns the Blake2 of it:
fn main() { let x = [163, 117, 178, 149] // some random bytes let hash = std::hash::blake2s(x); }
pedersen
Given an array of Fields, returns the Pedersen hash of it:
fn main() { let x = [163, 117, 178, 149] // some random bytes let hash = std::hash::pedersen(x); }
mimc_bn254 and mimc
mimc_bn254
is mimc
, but with hardcoded parameters for the BN254 curve. You can use it by providing an array of Fields, and it returns a Field with the hash. You can use the mimc
method if you're willing to input your own constants:
#![allow(unused)] fn main() { fn mimc<N>(x: Field, k: Field, constants: [Field; N], exp : Field) -> Field }
otherwise, use the mimc_bn254
method:
#![allow(unused)] fn main() { fn mimc_bn254<N>(array: [Field; N]) -> Field }
example:
fn main() { let x = [163, 117, 178, 149] // some random bytes let hash = std::hash::mimc_bn254(x); }
scalar_mul::fixed_base
Performs scalar multiplication over the embedded curve whose coordinates are defined by the configured noir field. For the BN254 scalar field, this is BabyJubJub or Grumpkin.
#![allow(unused)] fn main() { fn fixed_base(_input : Field) -> [Field; 2] }
example
fn main(x : Field) { let scal = std::scalar_mul::fixed_base(x); std::println(scal); }
schnorr::verify_signature
Verifier for Schnorr signatures
#![allow(unused)] fn main() { fn verify_signature(_public_key_x: Field, _public_key_y: Field, _signature: [u8; 64], _message: [u8]) -> Field }
ecdsa_secp256k1::verify_signature
Verifier for ECDSA Secp256k1 signatures
#![allow(unused)] fn main() { fn verify_signature(_public_key_x : [u8; 32], _public_key_y : [u8; 32], _signature: [u8; 64], _message: [u8]) -> Field }
Modules, Packages, Crates
In this section, we describe the package, crate and module system. As mentioned in the introduction, this will largely follow the design choice chosen by Rust.
Crate
A crate is the compilation unit used in Noir.
Crate Root
Every crate has a root, which is the source file that the compiler starts, this is also known as the root module.
The Noir compiler does not enforce any conditions on the name of the file which is the crate root, however if you are compiling via Nargo.
The Crate Root, must be called lib.nr
or main.nr
.
Packages
A Nargo Package is a collection of one of more crates. A Package must include a Nargo.toml file.
A Package must contain either a library or a binary crate.
Creating a new package
A new package is created using the new
command.
$ nargo new my-project
$ ls my-project
Nargo.toml
src
$ ls my-project/src
main.nr
Binary vs Library
Similar to Cargo, Nargo follows the convention that if there is a src/main.nr
then the project is a binary. If it contains a src/lib.nr
then it is a library.
However, note that dissimilar to Cargo, we cannot have both a binary and library in the same project.
Modules
Noir's module system follows the same convention as the newer version of Rust's module system.
Purpose of Modules
Modules are used to organise files. Without modules all of your code would need to live in a single file. In Noir, the compiler does not automatically scan all of your files to detect modules. This must be done explicitly by the developer.
Examples
Importing a module in the crate root
Filename : src/main.nr
mod foo;
fn main() {
foo::hello_world();
}
Filename : src/foo.nr
fn from_foo() {}
In the above snippet, the crate root is the src/main.nr
file.
The compiler sees the module declaration mod foo
which prompts it to look for a foo.nr file.
Visually this module hierarchy looks like the following :
crate
├── main
│
└── foo
└── from_foo
Sub-modules
Filename : src/main.nr
mod foo;
fn main() {
foo::from_foo();
}
Filename : src/foo.nr
mod bar;
fn from_foo() {}
Filename : src/foo/bar.nr
fn from_bar() {}
In the above snippet, we have added an extra module to the module tree; bar
. bar
is a submodule of foo
hence we declare bar in foo.nr
with mod bar
. Since foo
is not the crate root, the compiler looks for the file associated with the bar
module in src/foo/bar.nr
Visually the module hierarchy looks as follows:
crate
├── main
│
└── foo
├── from_foo
└── bar
└── from_bar
Dependencies
Nargo allows you to upload packages to GitHub and use them as dependencies.
Specifying a dependency
hello_world = { tag = "v0.5", git = "https://github.com/kevaundray/hello-world-noir"}
Specifying a dependency requires a tag to a specific commit and the git url to the url containing the package.
Currently, there are no requirements on the tag contents. If requirements are added, it would follow semver 2.0 guidelines.
Note: Without a
tag
, there would be no versioning and dependencies would change each time you compile your project.
ACIR (Abstract Circuit Intermediate Representation)
The purpose of ACIR is to act as an intermediate layer between the proof system that Noir chooses to compile to and the Noir syntax. This separation between proof system and programming language, allows those who want to integrate proof systems to have a stable target, moreover it allows the frontend to compile to any ACIR compatible proof system.
ACIR additionally allows proof systems to supply a fixed list of optimised blackbox functions that the frontend can access. Examples of this would be SHA256, PEDERSEN and SCHNORRSIGVERIFY.
Compiling a Proof
When inside of a given Noir project the command nargo compile my_proof
will perform two processes.
-
First, compile the Noir program to its ACIR and solve the circuit's witness.
-
Second, create a new
build/
directory to store the ACIR,my_proof.acir
, and the solved witness,my_proof.tr
These can be used by the Noir Typescript wrapper to generate a prover and verifier inside of Typescript rather than in Nargo. This will be discussed further in another section.
Features Coming Soon
Isize
Signed integers such as i32 and i64 allow one to express more circuits. They are partially supported but need to be finalised.
Recursion
Recursion is becoming feasible in circuits, hence Noir will have native support for it. Currently, only composition is supported.
LICENSE
Noir will be dual licensed under MIT/Apache (Version 2.0).